Skip to main content

mockforge_collab/
api.rs

1//! REST API endpoints for collaboration
2
3use crate::auth::{AuthService, Credentials};
4use crate::backup::{BackupService, StorageBackend};
5use crate::error::{CollabError, Result};
6use crate::history::VersionControl;
7use crate::merge::MergeService;
8use crate::middleware::{auth_middleware, AuthUser};
9use crate::models::UserRole;
10use crate::sync::SyncEngine;
11use crate::user::UserService;
12use crate::workspace::WorkspaceService;
13use axum::{
14    extract::{Path, Query, State},
15    http::StatusCode,
16    middleware,
17    response::{IntoResponse, Response},
18    routing::{delete, get, post, put},
19    Extension, Json, Router,
20};
21use serde::{Deserialize, Serialize};
22use std::sync::Arc;
23use uuid::Uuid;
24
25/// API state
26#[derive(Clone)]
27pub struct ApiState {
28    /// Authentication service
29    pub auth: Arc<AuthService>,
30    /// User service
31    pub user: Arc<UserService>,
32    /// Workspace service
33    pub workspace: Arc<WorkspaceService>,
34    /// Version control history
35    pub history: Arc<VersionControl>,
36    /// Merge service
37    pub merge: Arc<MergeService>,
38    /// Backup service
39    pub backup: Arc<BackupService>,
40    /// Sync engine
41    pub sync: Arc<SyncEngine>,
42}
43
44/// Create API router
45pub fn create_router(state: ApiState) -> Router {
46    // Public routes (no authentication required)
47    let public_routes = Router::new()
48        .route("/auth/register", post(register))
49        .route("/auth/login", post(login))
50        .route("/health", get(health_check))
51        .route("/ready", get(readiness_check));
52
53    // Protected routes (authentication required)
54    let protected_routes = Router::new()
55        // Workspaces
56        .route("/workspaces", post(create_workspace))
57        .route("/workspaces", get(list_workspaces))
58        .route("/workspaces/{id}", get(get_workspace))
59        .route("/workspaces/{id}", put(update_workspace))
60        .route("/workspaces/{id}", delete(delete_workspace))
61        // Members
62        .route("/workspaces/{id}/members", post(add_member))
63        .route("/workspaces/{id}/members/{user_id}", delete(remove_member))
64        .route("/workspaces/{id}/members/{user_id}/role", put(change_role))
65        .route("/workspaces/{id}/members", get(list_members))
66        // Version Control - Commits
67        .route("/workspaces/{id}/commits", post(create_commit))
68        .route("/workspaces/{id}/commits", get(list_commits))
69        .route("/workspaces/{id}/commits/{commit_id}", get(get_commit))
70        .route("/workspaces/{id}/restore/{commit_id}", post(restore_to_commit))
71        // Version Control - Snapshots
72        .route("/workspaces/{id}/snapshots", post(create_snapshot))
73        .route("/workspaces/{id}/snapshots", get(list_snapshots))
74        .route("/workspaces/{id}/snapshots/{name}", get(get_snapshot))
75        // Fork and Merge
76        .route("/workspaces/{id}/fork", post(fork_workspace))
77        .route("/workspaces/{id}/forks", get(list_forks))
78        .route("/workspaces/{id}/merge", post(merge_workspaces))
79        .route("/workspaces/{id}/merges", get(list_merges))
80        // Backup and Restore
81        .route("/workspaces/{id}/backup", post(create_backup))
82        .route("/workspaces/{id}/backups", get(list_backups))
83        .route("/workspaces/{id}/backups/{backup_id}", delete(delete_backup))
84        .route("/workspaces/{id}/restore", post(restore_workspace))
85        // State Management
86        .route("/workspaces/{id}/state", get(get_workspace_state))
87        .route("/workspaces/{id}/state", post(update_workspace_state))
88        .route("/workspaces/{id}/state/history", get(get_state_history))
89        .route_layer(middleware::from_fn_with_state(
90            state.auth.clone(),
91            auth_middleware,
92        ));
93
94    // Combine routes
95    Router::new().merge(public_routes).merge(protected_routes).with_state(state)
96}
97
98// ===== Request/Response Types =====
99
100/// User registration request
101#[derive(Debug, Deserialize)]
102pub struct RegisterRequest {
103    /// Username for the new account
104    pub username: String,
105    /// Email address
106    pub email: String,
107    /// Password
108    pub password: String,
109}
110
111/// Authentication response with JWT token
112#[derive(Debug, Serialize)]
113pub struct AuthResponse {
114    /// JWT access token
115    pub access_token: String,
116    /// Token type (Bearer)
117    pub token_type: String,
118    /// Token expiration timestamp
119    pub expires_at: String,
120}
121
122/// Request to create a new workspace
123#[derive(Debug, Deserialize)]
124pub struct CreateWorkspaceRequest {
125    /// Workspace name
126    pub name: String,
127    /// Optional description
128    pub description: Option<String>,
129}
130
131/// Request to update workspace settings
132#[derive(Debug, Deserialize)]
133pub struct UpdateWorkspaceRequest {
134    /// New workspace name
135    pub name: Option<String>,
136    /// New description
137    pub description: Option<String>,
138}
139
140/// Request to add a member to a workspace
141#[derive(Debug, Deserialize)]
142pub struct AddMemberRequest {
143    /// User ID to add
144    pub user_id: Uuid,
145    /// Role to assign
146    pub role: UserRole,
147}
148
149/// Request to change a member's role
150#[derive(Debug, Deserialize)]
151pub struct ChangeRoleRequest {
152    /// New role to assign
153    pub role: UserRole,
154}
155
156/// Request to create a new commit
157#[derive(Debug, Deserialize)]
158pub struct CreateCommitRequest {
159    /// Commit message
160    pub message: String,
161    /// Changes description as JSON
162    pub changes: serde_json::Value,
163}
164
165/// Request to create a named snapshot
166#[derive(Debug, Deserialize)]
167pub struct CreateSnapshotRequest {
168    /// Snapshot name
169    pub name: String,
170    /// Optional description
171    pub description: Option<String>,
172    /// Commit ID to snapshot
173    pub commit_id: Uuid,
174}
175
176/// Pagination query parameters
177#[derive(Debug, Deserialize)]
178pub struct PaginationQuery {
179    /// Number of items to return
180    #[serde(default = "default_limit")]
181    pub limit: i32,
182    /// Number of items to skip
183    #[serde(default)]
184    pub offset: i32,
185}
186
187const fn default_limit() -> i32 {
188    50
189}
190
191// ===== Error Handling =====
192
193impl IntoResponse for CollabError {
194    fn into_response(self) -> Response {
195        let (status, message) = match &self {
196            Self::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
197            Self::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg.clone()),
198            Self::WorkspaceNotFound(msg) | Self::UserNotFound(msg) => {
199                (StatusCode::NOT_FOUND, msg.clone())
200            }
201            Self::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
202            Self::AlreadyExists(msg) | Self::ConflictDetected(msg) => {
203                (StatusCode::CONFLICT, msg.clone())
204            }
205            Self::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg.clone()),
206            Self::Internal(msg)
207            | Self::DatabaseError(msg)
208            | Self::SerializationError(msg)
209            | Self::SyncError(msg)
210            | Self::WebSocketError(msg)
211            | Self::ConnectionError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
212            Self::VersionMismatch { expected, actual } => (
213                StatusCode::CONFLICT,
214                format!("Version mismatch: expected {expected}, got {actual}"),
215            ),
216        };
217
218        (status, Json(serde_json::json!({ "error": message }))).into_response()
219    }
220}
221
222// ===== Handler Functions =====
223
224/// Register a new user
225async fn register(
226    State(state): State<ApiState>,
227    Json(payload): Json<RegisterRequest>,
228) -> Result<Json<AuthResponse>> {
229    // Create user
230    let user = state
231        .user
232        .create_user(payload.username, payload.email, payload.password)
233        .await?;
234
235    // Generate token
236    let token = state.auth.generate_token(&user)?;
237
238    Ok(Json(AuthResponse {
239        access_token: token.access_token,
240        token_type: token.token_type,
241        expires_at: token.expires_at.to_rfc3339(),
242    }))
243}
244
245/// Login user
246async fn login(
247    State(state): State<ApiState>,
248    Json(payload): Json<Credentials>,
249) -> Result<Json<AuthResponse>> {
250    // Authenticate user
251    let user = state.user.authenticate(&payload.username, &payload.password).await?;
252
253    // Generate token
254    let token = state.auth.generate_token(&user)?;
255
256    Ok(Json(AuthResponse {
257        access_token: token.access_token,
258        token_type: token.token_type,
259        expires_at: token.expires_at.to_rfc3339(),
260    }))
261}
262
263/// Create a new workspace
264async fn create_workspace(
265    State(state): State<ApiState>,
266    Extension(auth_user): Extension<AuthUser>,
267    Json(payload): Json<CreateWorkspaceRequest>,
268) -> Result<Json<serde_json::Value>> {
269    // Create workspace
270    let workspace = state
271        .workspace
272        .create_workspace(payload.name, payload.description, auth_user.user_id)
273        .await?;
274
275    Ok(Json(serde_json::to_value(workspace)?))
276}
277
278/// List user's workspaces
279async fn list_workspaces(
280    State(state): State<ApiState>,
281    Extension(auth_user): Extension<AuthUser>,
282) -> Result<Json<serde_json::Value>> {
283    // List workspaces
284    let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
285
286    Ok(Json(serde_json::to_value(workspaces)?))
287}
288
289/// Get workspace by ID
290async fn get_workspace(
291    State(state): State<ApiState>,
292    Path(id): Path<Uuid>,
293    Extension(auth_user): Extension<AuthUser>,
294) -> Result<Json<serde_json::Value>> {
295    // Verify user is a member
296    let _member = state.workspace.get_member(id, auth_user.user_id).await?;
297
298    // Get workspace
299    let workspace = state.workspace.get_workspace(id).await?;
300
301    Ok(Json(serde_json::to_value(workspace)?))
302}
303
304/// Update workspace
305async fn update_workspace(
306    State(state): State<ApiState>,
307    Path(id): Path<Uuid>,
308    Extension(auth_user): Extension<AuthUser>,
309    Json(payload): Json<UpdateWorkspaceRequest>,
310) -> Result<Json<serde_json::Value>> {
311    // Update workspace (permission check inside)
312    let workspace = state
313        .workspace
314        .update_workspace(id, auth_user.user_id, payload.name, payload.description, None)
315        .await?;
316
317    Ok(Json(serde_json::to_value(workspace)?))
318}
319
320/// Delete workspace
321async fn delete_workspace(
322    State(state): State<ApiState>,
323    Path(id): Path<Uuid>,
324    Extension(auth_user): Extension<AuthUser>,
325) -> Result<StatusCode> {
326    // Delete workspace (permission check inside)
327    state.workspace.delete_workspace(id, auth_user.user_id).await?;
328
329    Ok(StatusCode::NO_CONTENT)
330}
331
332/// Add member to workspace
333async fn add_member(
334    State(state): State<ApiState>,
335    Path(workspace_id): Path<Uuid>,
336    Extension(auth_user): Extension<AuthUser>,
337    Json(payload): Json<AddMemberRequest>,
338) -> Result<Json<serde_json::Value>> {
339    // Add member (permission check inside)
340    let member = state
341        .workspace
342        .add_member(workspace_id, auth_user.user_id, payload.user_id, payload.role)
343        .await?;
344
345    Ok(Json(serde_json::to_value(member)?))
346}
347
348/// Remove member from workspace
349async fn remove_member(
350    State(state): State<ApiState>,
351    Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
352    Extension(auth_user): Extension<AuthUser>,
353) -> Result<StatusCode> {
354    // Remove member (permission check inside)
355    state
356        .workspace
357        .remove_member(workspace_id, auth_user.user_id, member_user_id)
358        .await?;
359
360    Ok(StatusCode::NO_CONTENT)
361}
362
363/// Change member role
364async fn change_role(
365    State(state): State<ApiState>,
366    Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
367    Extension(auth_user): Extension<AuthUser>,
368    Json(payload): Json<ChangeRoleRequest>,
369) -> Result<Json<serde_json::Value>> {
370    // Change role (permission check inside)
371    let member = state
372        .workspace
373        .change_role(workspace_id, auth_user.user_id, member_user_id, payload.role)
374        .await?;
375
376    Ok(Json(serde_json::to_value(member)?))
377}
378
379/// List workspace members
380async fn list_members(
381    State(state): State<ApiState>,
382    Path(workspace_id): Path<Uuid>,
383    Extension(auth_user): Extension<AuthUser>,
384) -> Result<Json<serde_json::Value>> {
385    // Verify user is a member
386    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
387
388    // List all members
389    let members = state.workspace.list_members(workspace_id).await?;
390
391    Ok(Json(serde_json::to_value(members)?))
392}
393
394/// Health check endpoint
395async fn health_check() -> impl IntoResponse {
396    Json(serde_json::json!({
397        "status": "healthy",
398        "timestamp": chrono::Utc::now().to_rfc3339(),
399    }))
400}
401
402/// Readiness check endpoint with database health check
403async fn readiness_check(State(state): State<ApiState>) -> impl IntoResponse {
404    // Check database connection by running a simple query
405    // We need to access the database pool - let's add it to ApiState or use workspace service
406    // For now, we'll use a workspace service method to check DB health
407    let db_healthy = state.workspace.check_database_health().await;
408
409    if db_healthy {
410        Json(serde_json::json!({
411            "status": "ready",
412            "database": "healthy",
413            "timestamp": chrono::Utc::now().to_rfc3339(),
414        }))
415        .into_response()
416    } else {
417        (
418            StatusCode::SERVICE_UNAVAILABLE,
419            Json(serde_json::json!({
420                "status": "not_ready",
421                "database": "unhealthy",
422                "timestamp": chrono::Utc::now().to_rfc3339(),
423            })),
424        )
425            .into_response()
426    }
427}
428
429// ===== Validation Helpers =====
430
431/// Validate commit message
432fn validate_commit_message(message: &str) -> Result<()> {
433    if message.is_empty() {
434        return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
435    }
436    if message.len() > 500 {
437        return Err(CollabError::InvalidInput(
438            "Commit message cannot exceed 500 characters".to_string(),
439        ));
440    }
441    Ok(())
442}
443
444/// Validate snapshot name
445fn validate_snapshot_name(name: &str) -> Result<()> {
446    if name.is_empty() {
447        return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
448    }
449    if name.len() > 100 {
450        return Err(CollabError::InvalidInput(
451            "Snapshot name cannot exceed 100 characters".to_string(),
452        ));
453    }
454    // Allow alphanumeric, hyphens, underscores, and dots
455    if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
456        return Err(CollabError::InvalidInput(
457            "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
458        ));
459    }
460    Ok(())
461}
462
463// ===== Version Control Handlers =====
464
465/// Create a commit in the workspace.
466///
467/// Creates a new commit capturing the current state of the workspace along with
468/// a description of changes. This is similar to `git commit`.
469///
470/// # Requirements
471/// - User must be a workspace member with Editor or Admin role
472/// - Commit message must be 1-500 characters
473///
474/// # Request Body
475/// - `message`: Commit message describing the changes (required, 1-500 chars)
476/// - `changes`: JSON object describing what changed
477///
478/// # Response
479/// Returns the created Commit object with:
480/// - `id`: Unique commit ID
481/// - `workspace_id`: ID of the workspace
482/// - `author_id`: ID of the user who created the commit
483/// - `message`: Commit message
484/// - `parent_id`: ID of the parent commit (null for first commit)
485/// - `version`: Version number (auto-incremented)
486/// - `snapshot`: Full workspace state at this commit
487/// - `changes`: Description of what changed
488/// - `created_at`: Timestamp
489///
490/// # Errors
491/// - `401 Unauthorized`: Not authenticated
492/// - `403 Forbidden`: User is not Editor or Admin
493/// - `404 Not Found`: Workspace not found or user not a member
494/// - `400 Bad Request`: Invalid commit message
495async fn create_commit(
496    State(state): State<ApiState>,
497    Path(workspace_id): Path<Uuid>,
498    Extension(auth_user): Extension<AuthUser>,
499    Json(payload): Json<CreateCommitRequest>,
500) -> Result<Json<serde_json::Value>> {
501    // Validate input
502    validate_commit_message(&payload.message)?;
503
504    // Verify user has permission (Editor or Admin)
505    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
506    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
507        return Err(CollabError::AuthorizationFailed(
508            "Only Admins and Editors can create commits".to_string(),
509        ));
510    }
511
512    // Get current workspace state
513    let workspace = state.workspace.get_workspace(workspace_id).await?;
514
515    // Get parent commit (latest)
516    let parent = state.history.get_latest_commit(workspace_id).await?;
517    let parent_id = parent.as_ref().map(|c| c.id);
518    let version = parent.as_ref().map_or(1, |c| c.version + 1);
519
520    // Create snapshot of current state
521    let snapshot = serde_json::to_value(&workspace)?;
522
523    // Create commit
524    let commit = state
525        .history
526        .create_commit(
527            workspace_id,
528            auth_user.user_id,
529            payload.message,
530            parent_id,
531            version,
532            snapshot,
533            payload.changes,
534        )
535        .await?;
536
537    Ok(Json(serde_json::to_value(commit)?))
538}
539
540/// List commits for a workspace.
541///
542/// Returns the commit history for a workspace in reverse chronological order
543/// (most recent first). Supports pagination via query parameters.
544///
545/// # Requirements
546/// - User must be a workspace member (any role)
547///
548/// # Query Parameters
549/// - `limit`: Number of commits to return (default: 50, max: 100)
550/// - `offset`: Number of commits to skip (default: 0)
551///
552/// # Response
553/// Returns a JSON object with:
554/// - `commits`: Array of Commit objects
555/// - `pagination`: Object with `limit` and `offset` values
556///
557/// # Example
558/// ```text
559/// GET /workspaces/{id}/commits?limit=20&offset=0
560/// ```
561///
562/// # Errors
563/// - `401 Unauthorized`: Not authenticated
564/// - `404 Not Found`: Workspace not found or user not a member
565async fn list_commits(
566    State(state): State<ApiState>,
567    Path(workspace_id): Path<Uuid>,
568    Extension(auth_user): Extension<AuthUser>,
569    Query(pagination): Query<PaginationQuery>,
570) -> Result<Json<serde_json::Value>> {
571    // Verify user is a member
572    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
573
574    // Validate pagination params
575    let limit = pagination.limit.clamp(1, 100);
576
577    // Get commit history
578    let commits = state.history.get_history(workspace_id, Some(limit)).await?;
579
580    // Return with pagination metadata
581    Ok(Json(serde_json::json!({
582        "commits": commits,
583        "pagination": {
584            "limit": limit,
585            "offset": pagination.offset,
586        }
587    })))
588}
589
590/// Get a specific commit by ID.
591///
592/// Retrieves detailed information about a specific commit, including the full
593/// workspace state snapshot at that point in time.
594///
595/// # Requirements
596/// - User must be a workspace member (any role)
597/// - Commit must belong to the specified workspace
598///
599/// # Errors
600/// - `401 Unauthorized`: Not authenticated
601/// - `404 Not Found`: Commit or workspace not found
602/// - `400 Bad Request`: Commit doesn't belong to this workspace
603async fn get_commit(
604    State(state): State<ApiState>,
605    Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
606    Extension(auth_user): Extension<AuthUser>,
607) -> Result<Json<serde_json::Value>> {
608    // Verify user is a member
609    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
610
611    // Get commit
612    let commit = state.history.get_commit(commit_id).await?;
613
614    // Verify commit belongs to this workspace
615    if commit.workspace_id != workspace_id {
616        return Err(CollabError::InvalidInput(
617            "Commit does not belong to this workspace".to_string(),
618        ));
619    }
620
621    Ok(Json(serde_json::to_value(commit)?))
622}
623
624/// Restore workspace to a specific commit.
625///
626/// Reverts the workspace to the state captured in the specified commit.
627/// This is a destructive operation that should be used carefully.
628///
629/// # Requirements
630/// - User must be a workspace member with Editor or Admin role
631/// - Commit must exist and belong to the workspace
632///
633/// # Response
634/// Returns an object with:
635/// - `workspace_id`: ID of the restored workspace
636/// - `commit_id`: ID of the commit that was restored
637/// - `restored_state`: The workspace state from the commit
638///
639/// # Errors
640/// - `401 Unauthorized`: Not authenticated
641/// - `403 Forbidden`: User is not Editor or Admin
642/// - `404 Not Found`: Commit or workspace not found
643async fn restore_to_commit(
644    State(state): State<ApiState>,
645    Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
646    Extension(auth_user): Extension<AuthUser>,
647) -> Result<Json<serde_json::Value>> {
648    // Verify user has permission (Editor or Admin)
649    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
650    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
651        return Err(CollabError::AuthorizationFailed(
652            "Only Admins and Editors can restore workspaces".to_string(),
653        ));
654    }
655
656    // Restore to commit
657    let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
658
659    Ok(Json(serde_json::json!({
660        "workspace_id": workspace_id,
661        "commit_id": commit_id,
662        "restored_state": restored_state
663    })))
664}
665
666/// Create a named snapshot.
667///
668/// Creates a named reference to a specific commit, similar to a git tag.
669/// Snapshots are useful for marking important states like releases.
670///
671/// # Requirements
672/// - User must be a workspace member with Editor or Admin role
673/// - Snapshot name must be 1-100 characters, alphanumeric with -, _, or .
674/// - Commit must exist
675///
676/// # Request Body
677/// - `name`: Name for the snapshot (required, 1-100 chars, alphanumeric)
678/// - `description`: Optional description
679/// - `commit_id`: ID of the commit to snapshot
680///
681/// # Errors
682/// - `401 Unauthorized`: Not authenticated
683/// - `403 Forbidden`: User is not Editor or Admin
684/// - `404 Not Found`: Workspace or commit not found
685/// - `400 Bad Request`: Invalid snapshot name
686async fn create_snapshot(
687    State(state): State<ApiState>,
688    Path(workspace_id): Path<Uuid>,
689    Extension(auth_user): Extension<AuthUser>,
690    Json(payload): Json<CreateSnapshotRequest>,
691) -> Result<Json<serde_json::Value>> {
692    // Validate input
693    validate_snapshot_name(&payload.name)?;
694
695    // Verify user has permission (Editor or Admin)
696    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
697    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
698        return Err(CollabError::AuthorizationFailed(
699            "Only Admins and Editors can create snapshots".to_string(),
700        ));
701    }
702
703    // Create snapshot
704    let snapshot = state
705        .history
706        .create_snapshot(
707            workspace_id,
708            payload.name,
709            payload.description,
710            payload.commit_id,
711            auth_user.user_id,
712        )
713        .await?;
714
715    Ok(Json(serde_json::to_value(snapshot)?))
716}
717
718/// List snapshots for a workspace.
719///
720/// Returns all named snapshots for the workspace in reverse chronological order.
721///
722/// # Requirements
723/// - User must be a workspace member (any role)
724///
725/// # Errors
726/// - `401 Unauthorized`: Not authenticated
727/// - `404 Not Found`: Workspace not found or user not a member
728async fn list_snapshots(
729    State(state): State<ApiState>,
730    Path(workspace_id): Path<Uuid>,
731    Extension(auth_user): Extension<AuthUser>,
732) -> Result<Json<serde_json::Value>> {
733    // Verify user is a member
734    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
735
736    // List snapshots
737    let snapshots = state.history.list_snapshots(workspace_id).await?;
738
739    Ok(Json(serde_json::to_value(snapshots)?))
740}
741
742/// Get a specific snapshot by name.
743///
744/// Retrieves details about a named snapshot, including which commit it references.
745///
746/// # Requirements
747/// - User must be a workspace member (any role)
748///
749/// # Errors
750/// - `401 Unauthorized`: Not authenticated
751/// - `404 Not Found`: Snapshot, workspace not found, or user not a member
752async fn get_snapshot(
753    State(state): State<ApiState>,
754    Path((workspace_id, name)): Path<(Uuid, String)>,
755    Extension(auth_user): Extension<AuthUser>,
756) -> Result<Json<serde_json::Value>> {
757    // Verify user is a member
758    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
759
760    // Get snapshot
761    let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
762
763    Ok(Json(serde_json::to_value(snapshot)?))
764}
765
766// ===== Fork and Merge Handlers =====
767
768/// Request to fork a workspace
769#[derive(Debug, Deserialize)]
770pub struct ForkWorkspaceRequest {
771    /// Name for the forked workspace
772    #[serde(alias = "new_name")]
773    pub name: Option<String>,
774    /// Commit ID to fork from
775    pub fork_point_commit_id: Option<Uuid>,
776}
777
778/// Fork a workspace
779async fn fork_workspace(
780    State(state): State<ApiState>,
781    Path(workspace_id): Path<Uuid>,
782    Extension(auth_user): Extension<AuthUser>,
783    Json(payload): Json<ForkWorkspaceRequest>,
784) -> Result<Json<serde_json::Value>> {
785    // Verify user has access to source workspace
786    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
787
788    // Fork workspace
789    let forked = state
790        .workspace
791        .fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
792        .await?;
793
794    Ok(Json(serde_json::to_value(forked)?))
795}
796
797/// List all forks of a workspace
798async fn list_forks(
799    State(state): State<ApiState>,
800    Path(workspace_id): Path<Uuid>,
801    Extension(auth_user): Extension<AuthUser>,
802) -> Result<Json<serde_json::Value>> {
803    // Verify user is a member
804    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
805
806    // List forks
807    let forks = state.workspace.list_forks(workspace_id).await?;
808
809    Ok(Json(serde_json::to_value(forks)?))
810}
811
812/// Request to merge workspaces
813#[derive(Debug, Deserialize)]
814pub struct MergeWorkspacesRequest {
815    /// Source workspace ID to merge from
816    pub source_workspace_id: Uuid,
817}
818
819/// Merge changes from another workspace
820async fn merge_workspaces(
821    State(state): State<ApiState>,
822    Path(target_workspace_id): Path<Uuid>,
823    Extension(auth_user): Extension<AuthUser>,
824    Json(payload): Json<MergeWorkspacesRequest>,
825) -> Result<Json<serde_json::Value>> {
826    // Verify user has permission to merge into target
827    let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
828    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
829        return Err(CollabError::AuthorizationFailed(
830            "Only Admins and Editors can merge workspaces".to_string(),
831        ));
832    }
833
834    // Perform merge
835    let (merged_state, conflicts) = state
836        .merge
837        .merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
838        .await?;
839
840    Ok(Json(serde_json::json!({
841        "workspace": merged_state,
842        "conflicts": conflicts,
843        "has_conflicts": !conflicts.is_empty()
844    })))
845}
846
847/// List merge operations for a workspace
848async fn list_merges(
849    State(state): State<ApiState>,
850    Path(workspace_id): Path<Uuid>,
851    Extension(auth_user): Extension<AuthUser>,
852) -> Result<Json<serde_json::Value>> {
853    // Verify user is a member
854    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
855
856    // List merges
857    let merges = state.merge.list_merges(workspace_id).await?;
858
859    Ok(Json(serde_json::to_value(merges)?))
860}
861
862// ===== Backup and Restore Handlers =====
863
864/// Request to create a workspace backup
865#[derive(Debug, Deserialize)]
866pub struct CreateBackupRequest {
867    /// Storage backend type
868    pub storage_backend: Option<String>,
869    /// Backup format
870    pub format: Option<String>,
871    /// Commit ID to backup
872    pub commit_id: Option<Uuid>,
873}
874
875/// Create a backup of a workspace
876#[allow(clippy::large_futures)]
877async fn create_backup(
878    State(state): State<ApiState>,
879    Path(workspace_id): Path<Uuid>,
880    Extension(auth_user): Extension<AuthUser>,
881    Json(payload): Json<CreateBackupRequest>,
882) -> Result<Json<serde_json::Value>> {
883    // Verify user has permission
884    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
885    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
886        return Err(CollabError::AuthorizationFailed(
887            "Only Admins and Editors can create backups".to_string(),
888        ));
889    }
890
891    // Determine storage backend
892    let storage_backend = match payload.storage_backend.as_deref() {
893        Some("s3") => StorageBackend::S3,
894        Some("azure") => StorageBackend::Azure,
895        Some("gcs") => StorageBackend::Gcs,
896        Some("custom") => StorageBackend::Custom,
897        _ => StorageBackend::Local,
898    };
899
900    // Create backup
901    let backup = state
902        .backup
903        .backup_workspace(
904            workspace_id,
905            auth_user.user_id,
906            storage_backend,
907            payload.format,
908            payload.commit_id,
909        )
910        .await?;
911
912    Ok(Json(serde_json::to_value(backup)?))
913}
914
915/// List backups for a workspace
916async fn list_backups(
917    State(state): State<ApiState>,
918    Path(workspace_id): Path<Uuid>,
919    Extension(auth_user): Extension<AuthUser>,
920    Query(pagination): Query<PaginationQuery>,
921) -> Result<Json<serde_json::Value>> {
922    // Verify user is a member
923    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
924
925    // List backups
926    let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
927
928    Ok(Json(serde_json::to_value(backups)?))
929}
930
931/// Delete a backup
932async fn delete_backup(
933    State(state): State<ApiState>,
934    Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
935    Extension(auth_user): Extension<AuthUser>,
936) -> Result<StatusCode> {
937    // Verify user has permission
938    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
939    if !matches!(member.role, UserRole::Admin) {
940        return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
941    }
942
943    // Delete backup
944    state.backup.delete_backup(backup_id).await?;
945
946    Ok(StatusCode::NO_CONTENT)
947}
948
949/// Request to restore a workspace from a backup
950#[derive(Debug, Deserialize)]
951pub struct RestoreWorkspaceRequest {
952    /// Backup ID to restore from
953    pub backup_id: Uuid,
954    /// Target workspace ID (creates new if None)
955    pub target_workspace_id: Option<Uuid>,
956}
957
958/// Restore a workspace from a backup
959async fn restore_workspace(
960    State(state): State<ApiState>,
961    Path(workspace_id): Path<Uuid>,
962    Extension(auth_user): Extension<AuthUser>,
963    Json(payload): Json<RestoreWorkspaceRequest>,
964) -> Result<Json<serde_json::Value>> {
965    // Verify user has permission
966    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
967    if !matches!(member.role, UserRole::Admin) {
968        return Err(CollabError::AuthorizationFailed(
969            "Only Admins can restore workspaces".to_string(),
970        ));
971    }
972
973    // Restore workspace
974    let restored_id = state
975        .backup
976        .restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
977        .await?;
978
979    Ok(Json(serde_json::json!({
980        "workspace_id": restored_id,
981        "restored_from_backup": payload.backup_id
982    })))
983}
984
985// ===== State Management Handlers =====
986
987/// Get current workspace state
988async fn get_workspace_state(
989    State(state): State<ApiState>,
990    Path(workspace_id): Path<Uuid>,
991    Extension(auth_user): Extension<AuthUser>,
992    Query(params): Query<std::collections::HashMap<String, String>>,
993) -> Result<Json<serde_json::Value>> {
994    // Verify user is a member
995    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
996
997    // Get version if specified
998    let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
999
1000    // Get state from sync engine - try full workspace state first
1001    let sync_state = if let Some(version) = version {
1002        state.sync.load_state_snapshot(workspace_id, Some(version)).await?
1003    } else {
1004        // Try to get full workspace state using CoreBridge
1005        if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
1006            // Get workspace for version info
1007            let workspace = state.workspace.get_workspace(workspace_id).await?;
1008            return Ok(Json(serde_json::json!({
1009                "workspace_id": workspace_id,
1010                "version": workspace.version,
1011                "state": full_state,
1012                "last_updated": workspace.updated_at
1013            })));
1014        }
1015
1016        // Fallback to in-memory state
1017        state.sync.get_state(workspace_id)
1018    };
1019
1020    if let Some(state_val) = sync_state {
1021        Ok(Json(serde_json::json!({
1022            "workspace_id": workspace_id,
1023            "version": state_val.version,
1024            "state": state_val.state,
1025            "last_updated": state_val.last_updated
1026        })))
1027    } else {
1028        // Return workspace metadata if no state available
1029        let workspace = state.workspace.get_workspace(workspace_id).await?;
1030        Ok(Json(serde_json::json!({
1031            "workspace_id": workspace_id,
1032            "version": workspace.version,
1033            "state": workspace.config,
1034            "last_updated": workspace.updated_at
1035        })))
1036    }
1037}
1038
1039/// Request to update workspace state via sync
1040#[derive(Debug, Deserialize)]
1041pub struct UpdateWorkspaceStateRequest {
1042    /// New workspace state as JSON
1043    pub state: serde_json::Value,
1044}
1045
1046/// Update workspace state
1047async fn update_workspace_state(
1048    State(state): State<ApiState>,
1049    Path(workspace_id): Path<Uuid>,
1050    Extension(auth_user): Extension<AuthUser>,
1051    Json(payload): Json<UpdateWorkspaceStateRequest>,
1052) -> Result<Json<serde_json::Value>> {
1053    // Verify user has permission
1054    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1055    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
1056        return Err(CollabError::AuthorizationFailed(
1057            "Only Admins and Editors can update workspace state".to_string(),
1058        ));
1059    }
1060
1061    // Update state in sync engine
1062    state.sync.update_state(workspace_id, payload.state.clone())?;
1063
1064    // Record state change
1065    let workspace = state.workspace.get_workspace(workspace_id).await?;
1066    state
1067        .sync
1068        .record_state_change(
1069            workspace_id,
1070            "full_sync",
1071            payload.state.clone(),
1072            workspace.version + 1,
1073            auth_user.user_id,
1074        )
1075        .await?;
1076
1077    Ok(Json(serde_json::json!({
1078        "workspace_id": workspace_id,
1079        "version": workspace.version + 1,
1080        "state": payload.state
1081    })))
1082}
1083
1084/// Get state change history
1085async fn get_state_history(
1086    State(state): State<ApiState>,
1087    Path(workspace_id): Path<Uuid>,
1088    Extension(auth_user): Extension<AuthUser>,
1089    Query(params): Query<std::collections::HashMap<String, String>>,
1090) -> Result<Json<serde_json::Value>> {
1091    // Verify user is a member
1092    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1093
1094    // Get since_version if specified
1095    let since_version =
1096        params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1097
1098    // Get state changes
1099    let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
1100
1101    Ok(Json(serde_json::json!({
1102        "workspace_id": workspace_id,
1103        "since_version": since_version,
1104        "changes": changes
1105    })))
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    #[tokio::test]
1111    async fn test_router_creation() {
1112        // Just ensure router can be created
1113        use super::*;
1114        use crate::core_bridge::CoreBridge;
1115        use crate::events::EventBus;
1116        use sqlx::SqlitePool;
1117        use tempfile::TempDir;
1118
1119        // Create temporary directory for test workspace and backup
1120        let temp_dir = TempDir::new().expect("Failed to create temp dir");
1121        let workspace_dir = temp_dir.path().join("workspaces");
1122        let backup_dir = temp_dir.path().join("backups");
1123        std::fs::create_dir_all(&workspace_dir).expect("Failed to create workspace dir");
1124        std::fs::create_dir_all(&backup_dir).expect("Failed to create backup dir");
1125
1126        // Use in-memory database for testing
1127        let db = SqlitePool::connect("sqlite::memory:")
1128            .await
1129            .expect("Failed to create database pool");
1130
1131        // Run migrations
1132        sqlx::migrate!("./migrations").run(&db).await.expect("Failed to run migrations");
1133
1134        // Create CoreBridge
1135        let core_bridge = Arc::new(CoreBridge::new(&workspace_dir));
1136
1137        // Create services
1138        let auth = Arc::new(AuthService::new("test-secret-key".to_string()));
1139        let user = Arc::new(UserService::new(db.clone(), auth.clone()));
1140        let workspace =
1141            Arc::new(WorkspaceService::with_core_bridge(db.clone(), core_bridge.clone()));
1142        let history = Arc::new(VersionControl::new(db.clone()));
1143        let merge = Arc::new(MergeService::new(db.clone()));
1144        let backup = Arc::new(BackupService::new(
1145            db.clone(),
1146            Some(backup_dir.to_string_lossy().to_string()),
1147            core_bridge,
1148            workspace.clone(),
1149        ));
1150        let event_bus = Arc::new(EventBus::new(100));
1151        let sync = Arc::new(SyncEngine::new(event_bus));
1152
1153        let state = ApiState {
1154            auth,
1155            user,
1156            workspace,
1157            history,
1158            merge,
1159            backup,
1160            sync,
1161        };
1162        let _router = create_router(state);
1163    }
1164}