pub mod account_deletion; pub mod ui; use axum::{ extract::{Path, Query, State}, response::IntoResponse, routing::{get, post}, Json, Router, }; use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::{ consent_history, cookie_consents, data_deletion_requests, data_export_requests, legal_acceptances, legal_document_versions, legal_documents, }; use crate::core::shared::state::AppState; #[derive(Debug, Clone, Queryable, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = legal_documents)] pub struct DbLegalDocument { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub slug: String, pub title: String, pub content: String, pub document_type: String, pub version: String, pub effective_date: DateTime, pub is_active: bool, pub requires_acceptance: bool, pub metadata: serde_json::Value, pub created_by: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Queryable, Insertable, Serialize, Deserialize)] #[diesel(table_name = legal_document_versions)] pub struct DbDocumentVersion { pub id: Uuid, pub document_id: Uuid, pub version: String, pub content: String, pub change_summary: Option, pub created_by: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Queryable, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = cookie_consents)] pub struct DbCookieConsent { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub user_id: Option, pub session_id: Option, pub ip_address: Option, pub user_agent: Option, pub country_code: Option, pub consent_necessary: bool, pub consent_analytics: bool, pub consent_marketing: bool, pub consent_preferences: bool, pub consent_functional: bool, pub consent_version: String, pub consent_given_at: DateTime, pub consent_updated_at: DateTime, pub consent_withdrawn_at: Option>, pub created_at: DateTime, } #[derive(Debug, Clone, Queryable, Insertable, Serialize, Deserialize)] #[diesel(table_name = consent_history)] pub struct DbConsentHistory { pub id: Uuid, pub consent_id: Uuid, pub action: String, pub previous_consents: serde_json::Value, pub new_consents: serde_json::Value, pub ip_address: Option, pub user_agent: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Queryable, Insertable, Serialize, Deserialize)] #[diesel(table_name = legal_acceptances)] pub struct DbLegalAcceptance { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub user_id: Uuid, pub document_id: Uuid, pub document_version: String, pub accepted_at: DateTime, pub ip_address: Option, pub user_agent: Option, } #[derive(Debug, Clone, Queryable, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = data_deletion_requests)] pub struct DbDeletionRequest { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub user_id: Uuid, pub request_type: String, pub status: String, pub reason: Option, pub requested_at: DateTime, pub scheduled_for: Option>, pub completed_at: Option>, pub confirmation_token: String, pub confirmed_at: Option>, pub processed_by: Option, pub notes: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Queryable, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = data_export_requests)] pub struct DbExportRequest { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub user_id: Uuid, pub status: String, pub format: String, pub include_sections: serde_json::Value, pub requested_at: DateTime, pub started_at: Option>, pub completed_at: Option>, pub file_url: Option, pub file_size: Option, pub expires_at: Option>, pub error_message: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum CookieCategory { Necessary, Analytics, Marketing, Preferences, Functional, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CookieConsent { pub id: Uuid, pub user_id: Option, pub session_id: Option, pub ip_address: Option, pub consents: HashMap, pub consent_given_at: DateTime, pub consent_updated_at: DateTime, pub consent_version: String, pub user_agent: Option, pub country_code: Option, } #[derive(Debug, Clone, Deserialize)] pub struct CookieConsentRequest { pub session_id: Option, pub consents: HashMap, pub user_agent: Option, } #[derive(Debug, Clone, Serialize)] pub struct CookieConsentResponse { pub id: Uuid, pub consents: HashMap, pub consent_given_at: DateTime, pub consent_version: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CookiePolicy { pub version: String, pub effective_date: DateTime, pub categories: Vec, pub last_updated: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CookieCategoryInfo { pub category: CookieCategory, pub name: String, pub description: String, pub is_required: bool, pub cookies: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CookieInfo { pub name: String, pub provider: String, pub purpose: String, pub expiry: String, pub cookie_type: CookieType, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum CookieType { Session, Persistent, FirstParty, ThirdParty, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LegalDocument { pub id: String, pub title: String, pub slug: String, pub content: String, pub version: String, pub effective_date: DateTime, pub last_updated: DateTime, pub document_type: LegalDocumentType, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LegalDocumentType { PrivacyPolicy, TermsOfService, CookiePolicy, AcceptableUsePolicy, DataProcessingAgreement, GdprCompliance, CcpaCompliance, } impl std::fmt::Display for LegalDocumentType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::PrivacyPolicy => "privacy_policy", Self::TermsOfService => "terms_of_service", Self::CookiePolicy => "cookie_policy", Self::AcceptableUsePolicy => "acceptable_use_policy", Self::DataProcessingAgreement => "data_processing_agreement", Self::GdprCompliance => "gdpr_compliance", Self::CcpaCompliance => "ccpa_compliance", }; write!(f, "{s}") } } impl std::str::FromStr for LegalDocumentType { type Err = String; fn from_str(s: &str) -> Result { match s { "privacy_policy" => Ok(Self::PrivacyPolicy), "terms_of_service" => Ok(Self::TermsOfService), "cookie_policy" => Ok(Self::CookiePolicy), "acceptable_use_policy" => Ok(Self::AcceptableUsePolicy), "data_processing_agreement" => Ok(Self::DataProcessingAgreement), "gdpr_compliance" => Ok(Self::GdprCompliance), "ccpa_compliance" => Ok(Self::CcpaCompliance), _ => Err(format!("Unknown document type: {s}")), } } } #[derive(Debug, Clone, Serialize)] pub struct DataDeletionResult { pub user_id: Uuid, pub consents_deleted: i32, pub deleted_at: DateTime, pub confirmation_token: String, } #[derive(Debug, Clone, Serialize)] pub struct UserDataExport { pub user_id: Uuid, pub exported_at: DateTime, pub consents: Vec, pub format: String, } #[derive(Debug, Deserialize)] pub struct ListDocumentsQuery { pub document_type: Option, pub active_only: Option, } #[derive(Debug, Deserialize)] pub struct CreateDocumentRequest { pub slug: String, pub title: String, pub content: String, pub document_type: LegalDocumentType, pub version: Option, pub requires_acceptance: Option, } #[derive(Debug, Deserialize)] pub struct UpdateDocumentRequest { pub title: Option, pub content: Option, pub is_active: Option, pub requires_acceptance: Option, } #[derive(Debug, Deserialize)] pub struct DataDeletionRequest { pub reason: Option, } #[derive(Debug, Deserialize)] pub struct DataExportRequest { pub format: Option, pub sections: Option>, } #[derive(Debug, thiserror::Error)] pub enum LegalError { #[error("Not found: {0}")] NotFound(String), #[error("Validation error: {0}")] Validation(String), #[error("Database error: {0}")] Database(String), #[error("Internal error: {0}")] Internal(String), } impl IntoResponse for LegalError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; let (status, message) = match &self { Self::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), Self::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()), Self::Database(msg) | Self::Internal(msg) => { (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()) } }; (status, Json(serde_json::json!({ "error": message }))).into_response() } } fn db_consent_to_consent(db: DbCookieConsent) -> CookieConsent { let mut consents = HashMap::new(); consents.insert(CookieCategory::Necessary, db.consent_necessary); consents.insert(CookieCategory::Analytics, db.consent_analytics); consents.insert(CookieCategory::Marketing, db.consent_marketing); consents.insert(CookieCategory::Preferences, db.consent_preferences); consents.insert(CookieCategory::Functional, db.consent_functional); CookieConsent { id: db.id, user_id: db.user_id, session_id: db.session_id, ip_address: db.ip_address, consents, consent_given_at: db.consent_given_at, consent_updated_at: db.consent_updated_at, consent_version: db.consent_version, user_agent: db.user_agent, country_code: db.country_code, } } fn db_document_to_document(db: DbLegalDocument) -> LegalDocument { let document_type: LegalDocumentType = db.document_type.parse().unwrap_or(LegalDocumentType::PrivacyPolicy); LegalDocument { id: db.id.to_string(), title: db.title, slug: db.slug, content: db.content, version: db.version, effective_date: db.effective_date, last_updated: db.updated_at, document_type, } } fn get_default_cookie_policy() -> CookiePolicy { let now = Utc::now(); CookiePolicy { version: "1.0.0".to_string(), effective_date: now, last_updated: now, categories: vec![ CookieCategoryInfo { category: CookieCategory::Necessary, name: "Necessary".to_string(), description: "Essential cookies required for the website to function properly.".to_string(), is_required: true, cookies: vec![ CookieInfo { name: "session_id".to_string(), provider: "General Bots".to_string(), purpose: "Session management".to_string(), expiry: "Session".to_string(), cookie_type: CookieType::Session, }, ], }, CookieCategoryInfo { category: CookieCategory::Analytics, name: "Analytics".to_string(), description: "Cookies that help us understand how visitors interact with our website.".to_string(), is_required: false, cookies: vec![], }, CookieCategoryInfo { category: CookieCategory::Marketing, name: "Marketing".to_string(), description: "Cookies used to deliver relevant advertisements.".to_string(), is_required: false, cookies: vec![], }, CookieCategoryInfo { category: CookieCategory::Preferences, name: "Preferences".to_string(), description: "Cookies that remember your preferences and settings.".to_string(), is_required: false, cookies: vec![], }, CookieCategoryInfo { category: CookieCategory::Functional, name: "Functional".to_string(), description: "Cookies that enable enhanced functionality.".to_string(), is_required: false, cookies: vec![], }, ], } } pub async fn handle_record_consent( State(state): State>, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); let now = Utc::now(); let mut consents = req.consents; consents.insert(CookieCategory::Necessary, true); let db_consent = DbCookieConsent { id: Uuid::new_v4(), org_id, bot_id, user_id: None, session_id: req.session_id, ip_address: None, user_agent: req.user_agent, country_code: None, consent_necessary: true, consent_analytics: *consents.get(&CookieCategory::Analytics).unwrap_or(&false), consent_marketing: *consents.get(&CookieCategory::Marketing).unwrap_or(&false), consent_preferences: *consents.get(&CookieCategory::Preferences).unwrap_or(&false), consent_functional: *consents.get(&CookieCategory::Functional).unwrap_or(&false), consent_version: "1.0.0".to_string(), consent_given_at: now, consent_updated_at: now, consent_withdrawn_at: None, created_at: now, }; diesel::insert_into(cookie_consents::table) .values(&db_consent) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(CookieConsentResponse { id: db_consent.id, consents, consent_given_at: db_consent.consent_given_at, consent_version: db_consent.consent_version, }) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_get_consent( State(state): State>, Path(consent_id): Path, ) -> Result>, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let db_consent: Option = cookie_consents::table .find(consent_id) .first(&mut conn) .optional() .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_consent.map(db_consent_to_consent)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } #[derive(Debug, Deserialize)] pub struct ConsentBySessionQuery { pub session_id: String, } pub async fn handle_get_consent_by_session( State(state): State>, Query(query): Query, ) -> Result>, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let db_consent: Option = cookie_consents::table .filter(cookie_consents::session_id.eq(&query.session_id)) .order(cookie_consents::consent_given_at.desc()) .first(&mut conn) .optional() .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_consent.map(db_consent_to_consent)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_update_consent( State(state): State>, Path(consent_id): Path, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let now = Utc::now(); let mut db_consent: DbCookieConsent = cookie_consents::table .find(consent_id) .first(&mut conn) .map_err(|_| LegalError::NotFound("Consent not found".to_string()))?; let previous_consents = serde_json::json!({ "necessary": db_consent.consent_necessary, "analytics": db_consent.consent_analytics, "marketing": db_consent.consent_marketing, "preferences": db_consent.consent_preferences, "functional": db_consent.consent_functional, }); let mut consents = req.consents; consents.insert(CookieCategory::Necessary, true); db_consent.consent_necessary = true; db_consent.consent_analytics = *consents.get(&CookieCategory::Analytics).unwrap_or(&false); db_consent.consent_marketing = *consents.get(&CookieCategory::Marketing).unwrap_or(&false); db_consent.consent_preferences = *consents.get(&CookieCategory::Preferences).unwrap_or(&false); db_consent.consent_functional = *consents.get(&CookieCategory::Functional).unwrap_or(&false); db_consent.consent_updated_at = now; let new_consents = serde_json::json!({ "necessary": db_consent.consent_necessary, "analytics": db_consent.consent_analytics, "marketing": db_consent.consent_marketing, "preferences": db_consent.consent_preferences, "functional": db_consent.consent_functional, }); diesel::update(cookie_consents::table.find(consent_id)) .set(&db_consent) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; let history = DbConsentHistory { id: Uuid::new_v4(), consent_id, action: "update".to_string(), previous_consents, new_consents, ip_address: None, user_agent: req.user_agent, created_at: now, }; diesel::insert_into(consent_history::table) .values(&history) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_consent_to_consent(db_consent)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_get_cookie_policy( State(_state): State>, ) -> Result, LegalError> { Ok(Json(get_default_cookie_policy())) } pub async fn handle_list_documents( State(state): State>, Query(query): Query, ) -> Result>, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _) = get_default_bot(&mut conn); let mut db_query = legal_documents::table .filter(legal_documents::bot_id.eq(bot_id)) .into_boxed(); if let Some(doc_type) = query.document_type { db_query = db_query.filter(legal_documents::document_type.eq(doc_type)); } if query.active_only.unwrap_or(true) { db_query = db_query.filter(legal_documents::is_active.eq(true)); } let db_docs: Vec = db_query .order(legal_documents::created_at.desc()) .load(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; let docs: Vec = db_docs.into_iter().map(db_document_to_document).collect(); Ok::<_, LegalError>(docs) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_get_document( State(state): State>, Path(slug): Path, ) -> Result>, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _) = get_default_bot(&mut conn); let db_doc: Option = legal_documents::table .filter(legal_documents::bot_id.eq(bot_id)) .filter(legal_documents::slug.eq(&slug)) .filter(legal_documents::is_active.eq(true)) .first(&mut conn) .optional() .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_doc.map(db_document_to_document)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_create_document( State(state): State>, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); let now = Utc::now(); let db_doc = DbLegalDocument { id: Uuid::new_v4(), org_id, bot_id, slug: req.slug, title: req.title, content: req.content, document_type: req.document_type.to_string(), version: req.version.unwrap_or_else(|| "1.0.0".to_string()), effective_date: now, is_active: true, requires_acceptance: req.requires_acceptance.unwrap_or(false), metadata: serde_json::json!({}), created_by: None, created_at: now, updated_at: now, }; diesel::insert_into(legal_documents::table) .values(&db_doc) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_document_to_document(db_doc)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_update_document( State(state): State>, Path(slug): Path, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _) = get_default_bot(&mut conn); let now = Utc::now(); let mut db_doc: DbLegalDocument = legal_documents::table .filter(legal_documents::bot_id.eq(bot_id)) .filter(legal_documents::slug.eq(&slug)) .first(&mut conn) .map_err(|_| LegalError::NotFound("Document not found".to_string()))?; if let Some(title) = req.title { db_doc.title = title; } if let Some(content) = req.content { let version_record = DbDocumentVersion { id: Uuid::new_v4(), document_id: db_doc.id, version: db_doc.version.clone(), content: db_doc.content.clone(), change_summary: None, created_by: None, created_at: now, }; diesel::insert_into(legal_document_versions::table) .values(&version_record) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; db_doc.content = content; } if let Some(is_active) = req.is_active { db_doc.is_active = is_active; } if let Some(requires_acceptance) = req.requires_acceptance { db_doc.requires_acceptance = requires_acceptance; } db_doc.updated_at = now; diesel::update(legal_documents::table.find(db_doc.id)) .set(&db_doc) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; Ok::<_, LegalError>(db_document_to_document(db_doc)) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_request_data_deletion( State(state): State>, Path(user_id): Path, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); let now = Utc::now(); let token = Uuid::new_v4().to_string(); let db_request = DbDeletionRequest { id: Uuid::new_v4(), org_id, bot_id, user_id, request_type: "full".to_string(), status: "pending".to_string(), reason: req.reason, requested_at: now, scheduled_for: Some(now + chrono::Duration::days(30)), completed_at: None, confirmation_token: token.clone(), confirmed_at: None, processed_by: None, notes: None, created_at: now, updated_at: now, }; diesel::insert_into(data_deletion_requests::table) .values(&db_request) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; let deleted_consents = diesel::delete( cookie_consents::table .filter(cookie_consents::bot_id.eq(bot_id)) .filter(cookie_consents::user_id.eq(user_id)), ) .execute(&mut conn) .unwrap_or(0); Ok::<_, LegalError>(DataDeletionResult { user_id, consents_deleted: deleted_consents as i32, deleted_at: now, confirmation_token: token, }) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub async fn handle_export_user_data( State(state): State>, Path(user_id): Path, Json(req): Json, ) -> Result, LegalError> { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().map_err(|e| LegalError::Database(e.to_string()))?; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); let now = Utc::now(); let format = req.format.unwrap_or_else(|| "json".to_string()); let sections = req.sections.unwrap_or_else(|| vec!["all".to_string()]); let db_export = DbExportRequest { id: Uuid::new_v4(), org_id, bot_id, user_id, status: "completed".to_string(), format: format.clone(), include_sections: serde_json::to_value(§ions).unwrap_or_default(), requested_at: now, started_at: Some(now), completed_at: Some(now), file_url: None, file_size: None, expires_at: Some(now + chrono::Duration::days(7)), error_message: None, created_at: now, }; diesel::insert_into(data_export_requests::table) .values(&db_export) .execute(&mut conn) .map_err(|e| LegalError::Database(e.to_string()))?; let db_consents: Vec = cookie_consents::table .filter(cookie_consents::bot_id.eq(bot_id)) .filter(cookie_consents::user_id.eq(user_id)) .load(&mut conn) .unwrap_or_default(); let consents: Vec = db_consents.into_iter().map(db_consent_to_consent).collect(); Ok::<_, LegalError>(UserDataExport { user_id, exported_at: now, consents, format, }) }) .await .map_err(|e| LegalError::Internal(e.to_string()))??; Ok(Json(result)) } pub struct LegalService {} impl LegalService { pub fn new() -> Self { Self {} } } impl Default for LegalService { fn default() -> Self { Self::new() } } pub fn configure_legal_routes() -> Router> { Router::new() .route("/api/legal/consent", post(handle_record_consent)) .route("/api/legal/consent/:consent_id", get(handle_get_consent).put(handle_update_consent)) .route("/api/legal/consent/session", get(handle_get_consent_by_session)) .route("/api/legal/cookies/policy", get(handle_get_cookie_policy)) .route("/api/legal/documents", get(handle_list_documents).post(handle_create_document)) .route("/api/legal/documents/:slug", get(handle_get_document).put(handle_update_document)) .route("/api/legal/gdpr/delete/:user_id", post(handle_request_data_deletion)) .route("/api/legal/gdpr/export/:user_id", post(handle_export_user_data)) }