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