pub mod ui; use axum::{ extract::{Path, Query, State}, http::StatusCode, routing::{get, post, put}, Json, Router, }; use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::{ canvas_collaborators, canvas_comments, canvas_elements, canvas_versions, canvases, }; use crate::core::shared::state::AppState; #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = canvases)] pub struct DbCanvas { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub name: String, pub description: Option, pub width: i32, pub height: i32, pub background_color: Option, pub thumbnail_url: Option, pub is_public: bool, pub is_template: bool, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = canvas_elements)] pub struct DbCanvasElement { pub id: Uuid, pub canvas_id: Uuid, pub element_type: String, pub x: f64, pub y: f64, pub width: f64, pub height: f64, pub rotation: f64, pub z_index: i32, pub locked: bool, pub properties: serde_json::Value, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = canvas_collaborators)] pub struct DbCanvasCollaborator { pub id: Uuid, pub canvas_id: Uuid, pub user_id: Uuid, pub permission: String, pub added_by: Option, pub added_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = canvas_versions)] pub struct DbCanvasVersion { pub id: Uuid, pub canvas_id: Uuid, pub version_number: i32, pub name: Option, pub elements_snapshot: serde_json::Value, pub created_by: Uuid, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = canvas_comments)] pub struct DbCanvasComment { pub id: Uuid, pub canvas_id: Uuid, pub element_id: Option, pub parent_comment_id: Option, pub author_id: Uuid, pub content: String, pub x_position: Option, pub y_position: Option, pub resolved: bool, pub resolved_by: Option, pub resolved_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Canvas { pub id: Uuid, pub org_id: Uuid, pub name: String, pub description: Option, pub width: i32, pub height: i32, pub background_color: String, pub thumbnail_url: Option, pub is_public: bool, pub is_template: bool, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, pub elements: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CanvasElement { pub id: Uuid, pub canvas_id: Uuid, pub element_type: ElementType, pub x: f64, pub y: f64, pub width: f64, pub height: f64, pub rotation: f64, pub z_index: i32, pub locked: bool, pub properties: ElementProperties, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ElementType { Rectangle, Ellipse, Line, Arrow, FreehandPath, Text, Image, Sticky, Frame, Connector, } impl ElementType { fn as_str(&self) -> &'static str { match self { Self::Rectangle => "rectangle", Self::Ellipse => "ellipse", Self::Line => "line", Self::Arrow => "arrow", Self::FreehandPath => "freehand_path", Self::Text => "text", Self::Image => "image", Self::Sticky => "sticky", Self::Frame => "frame", Self::Connector => "connector", } } fn from_str(s: &str) -> Self { match s { "rectangle" => Self::Rectangle, "ellipse" => Self::Ellipse, "line" => Self::Line, "arrow" => Self::Arrow, "freehand_path" => Self::FreehandPath, "text" => Self::Text, "image" => Self::Image, "sticky" => Self::Sticky, "frame" => Self::Frame, "connector" => Self::Connector, _ => Self::Rectangle, } } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ElementProperties { #[serde(skip_serializing_if = "Option::is_none")] pub fill_color: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stroke_color: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stroke_width: Option, #[serde(skip_serializing_if = "Option::is_none")] pub opacity: Option, #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, #[serde(skip_serializing_if = "Option::is_none")] pub font_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub font_family: Option, #[serde(skip_serializing_if = "Option::is_none")] pub text_align: Option, #[serde(skip_serializing_if = "Option::is_none")] pub image_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub path_data: Option, #[serde(skip_serializing_if = "Option::is_none")] pub corner_radius: Option, #[serde(skip_serializing_if = "Option::is_none")] pub start_arrow: Option, #[serde(skip_serializing_if = "Option::is_none")] pub end_arrow: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CanvasSummary { pub id: Uuid, pub name: String, pub description: Option, pub thumbnail_url: Option, pub element_count: i64, pub is_public: bool, pub is_template: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Deserialize)] pub struct CreateCanvasRequest { pub name: String, pub description: Option, pub width: Option, pub height: Option, pub background_color: Option, pub is_template: Option, } #[derive(Debug, Deserialize)] pub struct UpdateCanvasRequest { pub name: Option, pub description: Option, pub width: Option, pub height: Option, pub background_color: Option, pub is_public: Option, pub is_template: Option, } #[derive(Debug, Deserialize)] pub struct CreateElementRequest { pub element_type: ElementType, pub x: f64, pub y: f64, pub width: f64, pub height: f64, pub rotation: Option, pub properties: Option, pub z_index: Option, } #[derive(Debug, Deserialize)] pub struct UpdateElementRequest { pub x: Option, pub y: Option, pub width: Option, pub height: Option, pub rotation: Option, pub properties: Option, pub z_index: Option, pub locked: Option, } #[derive(Debug, Deserialize)] pub struct ExportRequest { pub format: ExportFormat, pub scale: Option, pub background: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ExportFormat { Png, Svg, Pdf, Json, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExportResponse { pub format: ExportFormat, pub url: Option, pub data: Option, } #[derive(Debug, Deserialize)] pub struct CreateCommentRequest { pub content: String, pub element_id: Option, pub parent_comment_id: Option, pub x_position: Option, pub y_position: Option, } #[derive(Debug, Deserialize)] pub struct AddCollaboratorRequest { pub user_id: Uuid, pub permission: Option, } #[derive(Debug, Deserialize)] pub struct ListQuery { pub search: Option, pub is_public: Option, pub is_template: Option, pub limit: Option, pub offset: Option, } #[derive(Debug, Serialize)] pub struct CollaborationSession { pub canvas_id: Uuid, pub user_id: Uuid, pub cursor_x: f64, pub cursor_y: f64, pub selection: Vec, pub connected_at: DateTime, } fn get_bot_context(state: &AppState) -> (Uuid, Uuid) { let Ok(mut conn) = state.conn.get() else { return (Uuid::nil(), Uuid::nil()); }; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); (org_id, bot_id) } fn db_to_canvas_element(db: DbCanvasElement) -> CanvasElement { let properties: ElementProperties = serde_json::from_value(db.properties).unwrap_or_default(); CanvasElement { id: db.id, canvas_id: db.canvas_id, element_type: ElementType::from_str(&db.element_type), x: db.x, y: db.y, width: db.width, height: db.height, rotation: db.rotation, z_index: db.z_index, locked: db.locked, properties, created_by: db.created_by, created_at: db.created_at, updated_at: db.updated_at, } } fn db_to_canvas(db: DbCanvas, elements: Vec) -> Canvas { Canvas { id: db.id, org_id: db.org_id, name: db.name, description: db.description, width: db.width, height: db.height, background_color: db.background_color.unwrap_or_else(|| "#ffffff".to_string()), thumbnail_url: db.thumbnail_url, is_public: db.is_public, is_template: db.is_template, created_by: db.created_by, created_at: db.created_at, updated_at: db.updated_at, elements, } } async fn list_canvases( State(state): State>, Query(query): Query, ) -> Result>, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let (org_id, bot_id) = get_bot_context(&state); let limit = query.limit.unwrap_or(50); let offset = query.offset.unwrap_or(0); let mut q = canvases::table .filter(canvases::org_id.eq(org_id)) .filter(canvases::bot_id.eq(bot_id)) .into_boxed(); if let Some(is_public) = query.is_public { q = q.filter(canvases::is_public.eq(is_public)); } if let Some(is_template) = query.is_template { q = q.filter(canvases::is_template.eq(is_template)); } if let Some(search) = query.search { let pattern = format!("%{search}%"); q = q.filter( canvases::name .ilike(pattern.clone()) .or(canvases::description.ilike(pattern)), ); } let db_canvases: Vec = q .order(canvases::updated_at.desc()) .limit(limit) .offset(offset) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; let mut summaries = Vec::with_capacity(db_canvases.len()); for c in db_canvases { let element_count: i64 = canvas_elements::table .filter(canvas_elements::canvas_id.eq(c.id)) .count() .get_result(&mut conn) .unwrap_or(0); summaries.push(CanvasSummary { id: c.id, name: c.name, description: c.description, thumbnail_url: c.thumbnail_url, element_count, is_public: c.is_public, is_template: c.is_template, created_at: c.created_at, updated_at: c.updated_at, }); } Ok(Json(summaries)) } async fn create_canvas( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let (org_id, bot_id) = get_bot_context(&state); let id = Uuid::new_v4(); let now = Utc::now(); let user_id = Uuid::nil(); let db_canvas = DbCanvas { id, org_id, bot_id, name: req.name, description: req.description, width: req.width.unwrap_or(1920), height: req.height.unwrap_or(1080), background_color: Some(req.background_color.unwrap_or_else(|| "#ffffff".to_string())), thumbnail_url: None, is_public: false, is_template: req.is_template.unwrap_or(false), created_by: user_id, created_at: now, updated_at: now, }; diesel::insert_into(canvases::table) .values(&db_canvas) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; let canvas = db_to_canvas(db_canvas, vec![]); Ok(Json(canvas)) } async fn get_canvas( State(state): State>, Path(canvas_id): Path, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let db_canvas: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let db_elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let elements: Vec = db_elements.into_iter().map(db_to_canvas_element).collect(); let canvas = db_to_canvas(db_canvas, elements); Ok(Json(canvas)) } async fn update_canvas( State(state): State>, Path(canvas_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let mut db_canvas: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; if let Some(name) = req.name { db_canvas.name = name; } if let Some(desc) = req.description { db_canvas.description = Some(desc); } if let Some(width) = req.width { db_canvas.width = width; } if let Some(height) = req.height { db_canvas.height = height; } if let Some(bg) = req.background_color { db_canvas.background_color = Some(bg); } if let Some(is_public) = req.is_public { db_canvas.is_public = is_public; } if let Some(is_template) = req.is_template { db_canvas.is_template = is_template; } db_canvas.updated_at = Utc::now(); diesel::update(canvases::table.filter(canvases::id.eq(canvas_id))) .set(&db_canvas) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; let db_elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let elements: Vec = db_elements.into_iter().map(db_to_canvas_element).collect(); let canvas = db_to_canvas(db_canvas, elements); Ok(Json(canvas)) } async fn delete_canvas( State(state): State>, Path(canvas_id): Path, ) -> Result { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; diesel::delete(canvas_comments::table.filter(canvas_comments::canvas_id.eq(canvas_id))) .execute(&mut conn) .ok(); diesel::delete(canvas_versions::table.filter(canvas_versions::canvas_id.eq(canvas_id))) .execute(&mut conn) .ok(); diesel::delete( canvas_collaborators::table.filter(canvas_collaborators::canvas_id.eq(canvas_id)), ) .execute(&mut conn) .ok(); diesel::delete(canvas_elements::table.filter(canvas_elements::canvas_id.eq(canvas_id))) .execute(&mut conn) .ok(); let deleted = diesel::delete(canvases::table.filter(canvases::id.eq(canvas_id))) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?; if deleted > 0 { Ok(StatusCode::NO_CONTENT) } else { Err((StatusCode::NOT_FOUND, "Canvas not found".to_string())) } } async fn list_elements( State(state): State>, Path(canvas_id): Path, ) -> Result>, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let _: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let db_elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; let elements: Vec = db_elements.into_iter().map(db_to_canvas_element).collect(); Ok(Json(elements)) } async fn create_element( State(state): State>, Path(canvas_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let _: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let now = Utc::now(); let user_id = Uuid::nil(); let id = Uuid::new_v4(); let max_z: Option = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .select(diesel::dsl::max(canvas_elements::z_index)) .first(&mut conn) .ok() .flatten(); let z_index = req.z_index.unwrap_or_else(|| max_z.unwrap_or(0) + 1); let properties = req.properties.unwrap_or_default(); let properties_json = serde_json::to_value(&properties).unwrap_or_else(|_| serde_json::json!({})); let db_element = DbCanvasElement { id, canvas_id, element_type: req.element_type.as_str().to_string(), x: req.x, y: req.y, width: req.width, height: req.height, rotation: req.rotation.unwrap_or(0.0), z_index, locked: false, properties: properties_json, created_by: user_id, created_at: now, updated_at: now, }; diesel::insert_into(canvas_elements::table) .values(&db_element) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; diesel::update(canvases::table.filter(canvases::id.eq(canvas_id))) .set(canvases::updated_at.eq(now)) .execute(&mut conn) .ok(); let element = db_to_canvas_element(db_element); Ok(Json(element)) } async fn update_element( State(state): State>, Path((canvas_id, element_id)): Path<(Uuid, Uuid)>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let mut db_element: DbCanvasElement = canvas_elements::table .filter(canvas_elements::id.eq(element_id)) .filter(canvas_elements::canvas_id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Element not found".to_string()))?; if let Some(x) = req.x { db_element.x = x; } if let Some(y) = req.y { db_element.y = y; } if let Some(width) = req.width { db_element.width = width; } if let Some(height) = req.height { db_element.height = height; } if let Some(rotation) = req.rotation { db_element.rotation = rotation; } if let Some(z_index) = req.z_index { db_element.z_index = z_index; } if let Some(locked) = req.locked { db_element.locked = locked; } if let Some(props) = req.properties { db_element.properties = serde_json::to_value(&props).unwrap_or_else(|_| serde_json::json!({})); } db_element.updated_at = Utc::now(); diesel::update(canvas_elements::table.filter(canvas_elements::id.eq(element_id))) .set(&db_element) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; diesel::update(canvases::table.filter(canvases::id.eq(canvas_id))) .set(canvases::updated_at.eq(Utc::now())) .execute(&mut conn) .ok(); let element = db_to_canvas_element(db_element); Ok(Json(element)) } async fn delete_element( State(state): State>, Path((canvas_id, element_id)): Path<(Uuid, Uuid)>, ) -> Result { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let deleted = diesel::delete( canvas_elements::table .filter(canvas_elements::id.eq(element_id)) .filter(canvas_elements::canvas_id.eq(canvas_id)), ) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?; if deleted > 0 { diesel::update(canvases::table.filter(canvases::id.eq(canvas_id))) .set(canvases::updated_at.eq(Utc::now())) .execute(&mut conn) .ok(); Ok(StatusCode::NO_CONTENT) } else { Err((StatusCode::NOT_FOUND, "Element not found".to_string())) } } async fn export_canvas( State(state): State>, Path(canvas_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let db_canvas: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let db_elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let elements: Vec = db_elements.into_iter().map(db_to_canvas_element).collect(); let canvas = db_to_canvas(db_canvas, elements); match req.format { ExportFormat::Json => { let json = serde_json::to_string_pretty(&canvas) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON error: {e}")))?; Ok(Json(ExportResponse { format: ExportFormat::Json, url: None, data: Some(json), })) } ExportFormat::Svg => { let svg = generate_svg(&canvas, req.background.unwrap_or(true)); Ok(Json(ExportResponse { format: ExportFormat::Svg, url: None, data: Some(svg), })) } _ => Ok(Json(ExportResponse { format: req.format, url: Some(format!("/api/canvas/{canvas_id}/export/file")), data: None, })), } } fn generate_svg(canvas: &Canvas, include_background: bool) -> String { let mut svg = format!( r##""##, canvas.width, canvas.height, canvas.width, canvas.height ); if include_background { svg.push_str(&format!( r##""##, canvas.background_color )); } for element in &canvas.elements { let transform = if element.rotation != 0.0 { format!( r##" transform="rotate({} {} {})""##, element.rotation, element.x + element.width / 2.0, element.y + element.height / 2.0 ) } else { String::new() }; let fill = element .properties .fill_color .as_deref() .unwrap_or("transparent"); let stroke = element .properties .stroke_color .as_deref() .unwrap_or("none"); let stroke_width = element.properties.stroke_width.unwrap_or(1.0); let opacity = element.properties.opacity.unwrap_or(1.0); match element.element_type { ElementType::Rectangle => { let radius = element.properties.corner_radius.unwrap_or(0.0); svg.push_str(&format!( r##""##, element.x, element.y, element.width, element.height, radius, fill, stroke, stroke_width, opacity, transform )); } ElementType::Ellipse => { svg.push_str(&format!( r##""##, element.x + element.width / 2.0, element.y + element.height / 2.0, element.width / 2.0, element.height / 2.0, fill, stroke, stroke_width, opacity, transform )); } ElementType::Text => { let text = element.properties.text.as_deref().unwrap_or(""); let font_size = element.properties.font_size.unwrap_or(16.0); let font_family = element .properties .font_family .as_deref() .unwrap_or("sans-serif"); svg.push_str(&format!( r##"{}"##, element.x, element.y + font_size, font_size, font_family, fill, opacity, transform, text )); } ElementType::FreehandPath => { if let Some(path_data) = &element.properties.path_data { svg.push_str(&format!( r##""##, path_data, stroke, stroke_width, opacity, transform )); } } ElementType::Line | ElementType::Arrow => { let marker = if element.element_type == ElementType::Arrow { r##" marker-end="url(#arrowhead)""## } else { "" }; svg.push_str(&format!( r##""##, element.x, element.y, element.x + element.width, element.y + element.height, stroke, stroke_width, opacity, marker, transform )); } _ => {} } } svg.push_str(""); svg } async fn list_collaborators( State(state): State>, Path(canvas_id): Path, ) -> Result>, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let collaborators: Vec = canvas_collaborators::table .filter(canvas_collaborators::canvas_id.eq(canvas_id)) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(collaborators)) } async fn add_collaborator( State(state): State>, Path(canvas_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let _: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let now = Utc::now(); let collaborator = DbCanvasCollaborator { id: Uuid::new_v4(), canvas_id, user_id: req.user_id, permission: req.permission.unwrap_or_else(|| "view".to_string()), added_by: None, added_at: now, }; diesel::insert_into(canvas_collaborators::table) .values(&collaborator) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(collaborator)) } async fn remove_collaborator( State(state): State>, Path((canvas_id, user_id)): Path<(Uuid, Uuid)>, ) -> Result { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let deleted = diesel::delete( canvas_collaborators::table .filter(canvas_collaborators::canvas_id.eq(canvas_id)) .filter(canvas_collaborators::user_id.eq(user_id)), ) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?; if deleted > 0 { Ok(StatusCode::NO_CONTENT) } else { Err((StatusCode::NOT_FOUND, "Collaborator not found".to_string())) } } async fn list_comments( State(state): State>, Path(canvas_id): Path, ) -> Result>, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let comments: Vec = canvas_comments::table .filter(canvas_comments::canvas_id.eq(canvas_id)) .order(canvas_comments::created_at.asc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(comments)) } async fn create_comment( State(state): State>, Path(canvas_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let _: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let now = Utc::now(); let user_id = Uuid::nil(); let comment = DbCanvasComment { id: Uuid::new_v4(), canvas_id, element_id: req.element_id, parent_comment_id: req.parent_comment_id, author_id: user_id, content: req.content, x_position: req.x_position, y_position: req.y_position, resolved: false, resolved_by: None, resolved_at: None, created_at: now, updated_at: now, }; diesel::insert_into(canvas_comments::table) .values(&comment) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(comment)) } async fn resolve_comment( State(state): State>, Path((canvas_id, comment_id)): Path<(Uuid, Uuid)>, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let now = Utc::now(); let user_id = Uuid::nil(); diesel::update( canvas_comments::table .filter(canvas_comments::id.eq(comment_id)) .filter(canvas_comments::canvas_id.eq(canvas_id)), ) .set(( canvas_comments::resolved.eq(true), canvas_comments::resolved_by.eq(Some(user_id)), canvas_comments::resolved_at.eq(Some(now)), canvas_comments::updated_at.eq(now), )) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; let comment: DbCanvasComment = canvas_comments::table .filter(canvas_comments::id.eq(comment_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Comment not found".to_string()))?; Ok(Json(comment)) } async fn list_versions( State(state): State>, Path(canvas_id): Path, ) -> Result>, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let versions: Vec = canvas_versions::table .filter(canvas_versions::canvas_id.eq(canvas_id)) .order(canvas_versions::version_number.desc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(versions)) } async fn create_version( State(state): State>, Path(canvas_id): Path, ) -> Result, (StatusCode, String)> { let mut conn = state .conn .get() .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let _: DbCanvas = canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Canvas not found".to_string()))?; let db_elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let max_version: Option = canvas_versions::table .filter(canvas_versions::canvas_id.eq(canvas_id)) .select(diesel::dsl::max(canvas_versions::version_number)) .first(&mut conn) .ok() .flatten(); let now = Utc::now(); let user_id = Uuid::nil(); let elements_snapshot = serde_json::to_value(&db_elements).unwrap_or_else(|_| serde_json::json!([])); let version = DbCanvasVersion { id: Uuid::new_v4(), canvas_id, version_number: max_version.unwrap_or(0) + 1, name: None, elements_snapshot, created_by: user_id, created_at: now, }; diesel::insert_into(canvas_versions::table) .values(&version) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(version)) } async fn get_collaboration_info( State(_state): State>, Path(canvas_id): Path, ) -> Result>, (StatusCode, String)> { let _ = canvas_id; Ok(Json(vec![])) } pub fn configure_canvas_routes() -> Router> { Router::new() .route("/api/canvas", get(list_canvases).post(create_canvas)) .route( "/api/canvas/{canvas_id}", get(get_canvas).put(update_canvas).delete(delete_canvas), ) .route( "/api/canvas/{canvas_id}/elements", get(list_elements).post(create_element), ) .route( "/api/canvas/{canvas_id}/elements/{element_id}", put(update_element).delete(delete_element), ) .route("/api/canvas/{canvas_id}/export", post(export_canvas)) .route( "/api/canvas/{canvas_id}/collaborators", get(list_collaborators).post(add_collaborator), ) .route( "/api/canvas/{canvas_id}/collaborators/{user_id}", axum::routing::delete(remove_collaborator), ) .route( "/api/canvas/{canvas_id}/comments", get(list_comments).post(create_comment), ) .route( "/api/canvas/{canvas_id}/comments/{comment_id}/resolve", put(resolve_comment), ) .route( "/api/canvas/{canvas_id}/versions", get(list_versions).post(create_version), ) .route( "/api/canvas/{canvas_id}/collaborate", get(get_collaboration_info), ) }