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
151fn 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            CollabError::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg),
161            CollabError::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg),
162            CollabError::WorkspaceNotFound(msg) => (StatusCode::NOT_FOUND, msg),
163            CollabError::UserNotFound(msg) => (StatusCode::NOT_FOUND, msg),
164            CollabError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
165            CollabError::AlreadyExists(msg) => (StatusCode::CONFLICT, msg),
166            CollabError::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    StatusCode::OK
349}
350
351/// Readiness check endpoint
352async fn readiness_check() -> impl IntoResponse {
353    // TODO: Check database connection
354    StatusCode::OK
355}
356
357// ===== Validation Helpers =====
358
359/// Validate commit message
360fn validate_commit_message(message: &str) -> Result<()> {
361    if message.is_empty() {
362        return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
363    }
364    if message.len() > 500 {
365        return Err(CollabError::InvalidInput(
366            "Commit message cannot exceed 500 characters".to_string(),
367        ));
368    }
369    Ok(())
370}
371
372/// Validate snapshot name
373fn validate_snapshot_name(name: &str) -> Result<()> {
374    if name.is_empty() {
375        return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
376    }
377    if name.len() > 100 {
378        return Err(CollabError::InvalidInput(
379            "Snapshot name cannot exceed 100 characters".to_string(),
380        ));
381    }
382    // Allow alphanumeric, hyphens, underscores, and dots
383    if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
384        return Err(CollabError::InvalidInput(
385            "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
386        ));
387    }
388    Ok(())
389}
390
391// ===== Version Control Handlers =====
392
393/// Create a commit in the workspace.
394///
395/// Creates a new commit capturing the current state of the workspace along with
396/// a description of changes. This is similar to `git commit`.
397///
398/// # Requirements
399/// - User must be a workspace member with Editor or Admin role
400/// - Commit message must be 1-500 characters
401///
402/// # Request Body
403/// - `message`: Commit message describing the changes (required, 1-500 chars)
404/// - `changes`: JSON object describing what changed
405///
406/// # Response
407/// Returns the created Commit object with:
408/// - `id`: Unique commit ID
409/// - `workspace_id`: ID of the workspace
410/// - `author_id`: ID of the user who created the commit
411/// - `message`: Commit message
412/// - `parent_id`: ID of the parent commit (null for first commit)
413/// - `version`: Version number (auto-incremented)
414/// - `snapshot`: Full workspace state at this commit
415/// - `changes`: Description of what changed
416/// - `created_at`: Timestamp
417///
418/// # Errors
419/// - `401 Unauthorized`: Not authenticated
420/// - `403 Forbidden`: User is not Editor or Admin
421/// - `404 Not Found`: Workspace not found or user not a member
422/// - `400 Bad Request`: Invalid commit message
423async fn create_commit(
424    State(state): State<ApiState>,
425    Path(workspace_id): Path<Uuid>,
426    Extension(auth_user): Extension<AuthUser>,
427    Json(payload): Json<CreateCommitRequest>,
428) -> Result<Json<serde_json::Value>> {
429    // Validate input
430    validate_commit_message(&payload.message)?;
431
432    // Verify user has permission (Editor or Admin)
433    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
434    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
435        return Err(CollabError::AuthorizationFailed(
436            "Only Admins and Editors can create commits".to_string(),
437        ));
438    }
439
440    // Get current workspace state
441    let workspace = state.workspace.get_workspace(workspace_id).await?;
442
443    // Get parent commit (latest)
444    let parent = state.history.get_latest_commit(workspace_id).await?;
445    let parent_id = parent.as_ref().map(|c| c.id);
446    let version = parent.as_ref().map(|c| c.version + 1).unwrap_or(1);
447
448    // Create snapshot of current state
449    let snapshot = serde_json::to_value(&workspace)?;
450
451    // Create commit
452    let commit = state
453        .history
454        .create_commit(
455            workspace_id,
456            auth_user.user_id,
457            payload.message,
458            parent_id,
459            version,
460            snapshot,
461            payload.changes,
462        )
463        .await?;
464
465    Ok(Json(serde_json::to_value(commit)?))
466}
467
468/// List commits for a workspace.
469///
470/// Returns the commit history for a workspace in reverse chronological order
471/// (most recent first). Supports pagination via query parameters.
472///
473/// # Requirements
474/// - User must be a workspace member (any role)
475///
476/// # Query Parameters
477/// - `limit`: Number of commits to return (default: 50, max: 100)
478/// - `offset`: Number of commits to skip (default: 0)
479///
480/// # Response
481/// Returns a JSON object with:
482/// - `commits`: Array of Commit objects
483/// - `pagination`: Object with `limit` and `offset` values
484///
485/// # Example
486/// ```
487/// GET /workspaces/{id}/commits?limit=20&offset=0
488/// ```
489///
490/// # Errors
491/// - `401 Unauthorized`: Not authenticated
492/// - `404 Not Found`: Workspace not found or user not a member
493async fn list_commits(
494    State(state): State<ApiState>,
495    Path(workspace_id): Path<Uuid>,
496    Extension(auth_user): Extension<AuthUser>,
497    Query(pagination): Query<PaginationQuery>,
498) -> Result<Json<serde_json::Value>> {
499    // Verify user is a member
500    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
501
502    // Validate pagination params
503    let limit = pagination.limit.clamp(1, 100);
504
505    // Get commit history
506    let commits = state.history.get_history(workspace_id, Some(limit)).await?;
507
508    // Return with pagination metadata
509    Ok(Json(serde_json::json!({
510        "commits": commits,
511        "pagination": {
512            "limit": limit,
513            "offset": pagination.offset,
514        }
515    })))
516}
517
518/// Get a specific commit by ID.
519///
520/// Retrieves detailed information about a specific commit, including the full
521/// workspace state snapshot at that point in time.
522///
523/// # Requirements
524/// - User must be a workspace member (any role)
525/// - Commit must belong to the specified workspace
526///
527/// # Errors
528/// - `401 Unauthorized`: Not authenticated
529/// - `404 Not Found`: Commit or workspace not found
530/// - `400 Bad Request`: Commit doesn't belong to this workspace
531async fn get_commit(
532    State(state): State<ApiState>,
533    Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
534    Extension(auth_user): Extension<AuthUser>,
535) -> Result<Json<serde_json::Value>> {
536    // Verify user is a member
537    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
538
539    // Get commit
540    let commit = state.history.get_commit(commit_id).await?;
541
542    // Verify commit belongs to this workspace
543    if commit.workspace_id != workspace_id {
544        return Err(CollabError::InvalidInput(
545            "Commit does not belong to this workspace".to_string(),
546        ));
547    }
548
549    Ok(Json(serde_json::to_value(commit)?))
550}
551
552/// Restore workspace to a specific commit.
553///
554/// Reverts the workspace to the state captured in the specified commit.
555/// This is a destructive operation that should be used carefully.
556///
557/// # Requirements
558/// - User must be a workspace member with Editor or Admin role
559/// - Commit must exist and belong to the workspace
560///
561/// # Response
562/// Returns an object with:
563/// - `workspace_id`: ID of the restored workspace
564/// - `commit_id`: ID of the commit that was restored
565/// - `restored_state`: The workspace state from the commit
566///
567/// # Errors
568/// - `401 Unauthorized`: Not authenticated
569/// - `403 Forbidden`: User is not Editor or Admin
570/// - `404 Not Found`: Commit or workspace not found
571async fn restore_to_commit(
572    State(state): State<ApiState>,
573    Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
574    Extension(auth_user): Extension<AuthUser>,
575) -> Result<Json<serde_json::Value>> {
576    // Verify user has permission (Editor or Admin)
577    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
578    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
579        return Err(CollabError::AuthorizationFailed(
580            "Only Admins and Editors can restore workspaces".to_string(),
581        ));
582    }
583
584    // Restore to commit
585    let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
586
587    Ok(Json(serde_json::json!({
588        "workspace_id": workspace_id,
589        "commit_id": commit_id,
590        "restored_state": restored_state
591    })))
592}
593
594/// Create a named snapshot.
595///
596/// Creates a named reference to a specific commit, similar to a git tag.
597/// Snapshots are useful for marking important states like releases.
598///
599/// # Requirements
600/// - User must be a workspace member with Editor or Admin role
601/// - Snapshot name must be 1-100 characters, alphanumeric with -, _, or .
602/// - Commit must exist
603///
604/// # Request Body
605/// - `name`: Name for the snapshot (required, 1-100 chars, alphanumeric)
606/// - `description`: Optional description
607/// - `commit_id`: ID of the commit to snapshot
608///
609/// # Errors
610/// - `401 Unauthorized`: Not authenticated
611/// - `403 Forbidden`: User is not Editor or Admin
612/// - `404 Not Found`: Workspace or commit not found
613/// - `400 Bad Request`: Invalid snapshot name
614async fn create_snapshot(
615    State(state): State<ApiState>,
616    Path(workspace_id): Path<Uuid>,
617    Extension(auth_user): Extension<AuthUser>,
618    Json(payload): Json<CreateSnapshotRequest>,
619) -> Result<Json<serde_json::Value>> {
620    // Validate input
621    validate_snapshot_name(&payload.name)?;
622
623    // Verify user has permission (Editor or Admin)
624    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
625    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
626        return Err(CollabError::AuthorizationFailed(
627            "Only Admins and Editors can create snapshots".to_string(),
628        ));
629    }
630
631    // Create snapshot
632    let snapshot = state
633        .history
634        .create_snapshot(
635            workspace_id,
636            payload.name,
637            payload.description,
638            payload.commit_id,
639            auth_user.user_id,
640        )
641        .await?;
642
643    Ok(Json(serde_json::to_value(snapshot)?))
644}
645
646/// List snapshots for a workspace.
647///
648/// Returns all named snapshots for the workspace in reverse chronological order.
649///
650/// # Requirements
651/// - User must be a workspace member (any role)
652///
653/// # Errors
654/// - `401 Unauthorized`: Not authenticated
655/// - `404 Not Found`: Workspace not found or user not a member
656async fn list_snapshots(
657    State(state): State<ApiState>,
658    Path(workspace_id): Path<Uuid>,
659    Extension(auth_user): Extension<AuthUser>,
660) -> Result<Json<serde_json::Value>> {
661    // Verify user is a member
662    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
663
664    // List snapshots
665    let snapshots = state.history.list_snapshots(workspace_id).await?;
666
667    Ok(Json(serde_json::to_value(snapshots)?))
668}
669
670/// Get a specific snapshot by name.
671///
672/// Retrieves details about a named snapshot, including which commit it references.
673///
674/// # Requirements
675/// - User must be a workspace member (any role)
676///
677/// # Errors
678/// - `401 Unauthorized`: Not authenticated
679/// - `404 Not Found`: Snapshot, workspace not found, or user not a member
680async fn get_snapshot(
681    State(state): State<ApiState>,
682    Path((workspace_id, name)): Path<(Uuid, String)>,
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    // Get snapshot
689    let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
690
691    Ok(Json(serde_json::to_value(snapshot)?))
692}
693
694// ===== Fork and Merge Handlers =====
695
696#[derive(Debug, Deserialize)]
697pub struct ForkWorkspaceRequest {
698    pub name: Option<String>,
699    pub fork_point_commit_id: Option<Uuid>,
700}
701
702/// Fork a workspace
703async fn fork_workspace(
704    State(state): State<ApiState>,
705    Path(workspace_id): Path<Uuid>,
706    Extension(auth_user): Extension<AuthUser>,
707    Json(payload): Json<ForkWorkspaceRequest>,
708) -> Result<Json<serde_json::Value>> {
709    // Verify user has access to source workspace
710    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
711
712    // Fork workspace
713    let forked = state
714        .workspace
715        .fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
716        .await?;
717
718    Ok(Json(serde_json::to_value(forked)?))
719}
720
721/// List all forks of a workspace
722async fn list_forks(
723    State(state): State<ApiState>,
724    Path(workspace_id): Path<Uuid>,
725    Extension(auth_user): Extension<AuthUser>,
726) -> Result<Json<serde_json::Value>> {
727    // Verify user is a member
728    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
729
730    // List forks
731    let forks = state.workspace.list_forks(workspace_id).await?;
732
733    Ok(Json(serde_json::to_value(forks)?))
734}
735
736#[derive(Debug, Deserialize)]
737pub struct MergeWorkspacesRequest {
738    pub source_workspace_id: Uuid,
739}
740
741/// Merge changes from another workspace
742async fn merge_workspaces(
743    State(state): State<ApiState>,
744    Path(target_workspace_id): Path<Uuid>,
745    Extension(auth_user): Extension<AuthUser>,
746    Json(payload): Json<MergeWorkspacesRequest>,
747) -> Result<Json<serde_json::Value>> {
748    // Verify user has permission to merge into target
749    let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
750    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
751        return Err(CollabError::AuthorizationFailed(
752            "Only Admins and Editors can merge workspaces".to_string(),
753        ));
754    }
755
756    // Perform merge
757    let (merged_state, conflicts) = state
758        .merge
759        .merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
760        .await?;
761
762    Ok(Json(serde_json::json!({
763        "merged_state": merged_state,
764        "conflicts": conflicts,
765        "has_conflicts": !conflicts.is_empty()
766    })))
767}
768
769/// List merge operations for a workspace
770async fn list_merges(
771    State(state): State<ApiState>,
772    Path(workspace_id): Path<Uuid>,
773    Extension(auth_user): Extension<AuthUser>,
774) -> Result<Json<serde_json::Value>> {
775    // Verify user is a member
776    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
777
778    // List merges
779    let merges = state.merge.list_merges(workspace_id).await?;
780
781    Ok(Json(serde_json::to_value(merges)?))
782}
783
784// ===== Backup and Restore Handlers =====
785
786#[derive(Debug, Deserialize)]
787pub struct CreateBackupRequest {
788    pub storage_backend: Option<String>,
789    pub format: Option<String>,
790    pub commit_id: Option<Uuid>,
791}
792
793/// Create a backup of a workspace
794async fn create_backup(
795    State(state): State<ApiState>,
796    Path(workspace_id): Path<Uuid>,
797    Extension(auth_user): Extension<AuthUser>,
798    Json(payload): Json<CreateBackupRequest>,
799) -> Result<Json<serde_json::Value>> {
800    // Verify user has permission
801    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
802    if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
803        return Err(CollabError::AuthorizationFailed(
804            "Only Admins and Editors can create backups".to_string(),
805        ));
806    }
807
808    // Determine storage backend
809    let storage_backend = match payload.storage_backend.as_deref() {
810        Some("s3") => StorageBackend::S3,
811        Some("azure") => StorageBackend::Azure,
812        Some("gcs") => StorageBackend::Gcs,
813        Some("custom") => StorageBackend::Custom,
814        _ => StorageBackend::Local,
815    };
816
817    // Create backup
818    let backup = state
819        .backup
820        .backup_workspace(
821            workspace_id,
822            auth_user.user_id,
823            storage_backend,
824            payload.format,
825            payload.commit_id,
826        )
827        .await?;
828
829    Ok(Json(serde_json::to_value(backup)?))
830}
831
832/// List backups for a workspace
833async fn list_backups(
834    State(state): State<ApiState>,
835    Path(workspace_id): Path<Uuid>,
836    Extension(auth_user): Extension<AuthUser>,
837    Query(pagination): Query<PaginationQuery>,
838) -> Result<Json<serde_json::Value>> {
839    // Verify user is a member
840    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
841
842    // List backups
843    let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
844
845    Ok(Json(serde_json::to_value(backups)?))
846}
847
848/// Delete a backup
849async fn delete_backup(
850    State(state): State<ApiState>,
851    Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
852    Extension(auth_user): Extension<AuthUser>,
853) -> Result<StatusCode> {
854    // Verify user has permission
855    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
856    if !matches!(member.role, UserRole::Admin) {
857        return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
858    }
859
860    // Delete backup
861    state.backup.delete_backup(backup_id).await?;
862
863    Ok(StatusCode::NO_CONTENT)
864}
865
866#[derive(Debug, Deserialize)]
867pub struct RestoreWorkspaceRequest {
868    pub backup_id: Uuid,
869    pub target_workspace_id: Option<Uuid>,
870}
871
872/// Restore a workspace from a backup
873async fn restore_workspace(
874    State(state): State<ApiState>,
875    Path(workspace_id): Path<Uuid>,
876    Extension(auth_user): Extension<AuthUser>,
877    Json(payload): Json<RestoreWorkspaceRequest>,
878) -> Result<Json<serde_json::Value>> {
879    // Verify user has permission
880    let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
881    if !matches!(member.role, UserRole::Admin) {
882        return Err(CollabError::AuthorizationFailed(
883            "Only Admins can restore workspaces".to_string(),
884        ));
885    }
886
887    // Restore workspace
888    let restored_id = state
889        .backup
890        .restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
891        .await?;
892
893    Ok(Json(serde_json::json!({
894        "workspace_id": restored_id,
895        "restored_from_backup": payload.backup_id
896    })))
897}
898
899// ===== State Management Handlers =====
900
901/// Get current workspace state
902async fn get_workspace_state(
903    State(state): State<ApiState>,
904    Path(workspace_id): Path<Uuid>,
905    Extension(auth_user): Extension<AuthUser>,
906    Query(params): Query<std::collections::HashMap<String, String>>,
907) -> Result<Json<serde_json::Value>> {
908    // Verify user is a member
909    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
910
911    // Get version if specified
912    let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
913
914    // Get state from sync engine - try full workspace state first
915    let sync_state = if let Some(version) = version {
916        state.sync.load_state_snapshot(workspace_id, Some(version)).await?
917    } else {
918        // Try to get full workspace state using CoreBridge
919        if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
920            // Get workspace for version info
921            let workspace = state.workspace.get_workspace(workspace_id).await?;
922            return Ok(Json(serde_json::json!({
923                "workspace_id": workspace_id,
924                "version": workspace.version,
925                "state": full_state,
926                "last_updated": workspace.updated_at
927            })));
928        }
929
930        // Fallback to in-memory state
931        state.sync.get_state(workspace_id)
932    };
933
934    if let Some(state_val) = sync_state {
935        Ok(Json(serde_json::json!({
936            "workspace_id": workspace_id,
937            "version": state_val.version,
938            "state": state_val.state,
939            "last_updated": state_val.last_updated
940        })))
941    } else {
942        // Return workspace metadata if no state available
943        let workspace = state.workspace.get_workspace(workspace_id).await?;
944        Ok(Json(serde_json::json!({
945            "workspace_id": workspace_id,
946            "version": workspace.version,
947            "state": workspace.config,
948            "last_updated": workspace.updated_at
949        })))
950    }
951}
952
953#[derive(Debug, Deserialize)]
954pub struct UpdateWorkspaceStateRequest {
955    pub state: serde_json::Value,
956}
957
958/// Update workspace state
959async fn update_workspace_state(
960    State(state): State<ApiState>,
961    Path(workspace_id): Path<Uuid>,
962    Extension(auth_user): Extension<AuthUser>,
963    Json(payload): Json<UpdateWorkspaceStateRequest>,
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 | UserRole::Editor) {
968        return Err(CollabError::AuthorizationFailed(
969            "Only Admins and Editors can update workspace state".to_string(),
970        ));
971    }
972
973    // Update state in sync engine
974    state.sync.update_state(workspace_id, payload.state.clone())?;
975
976    // Record state change
977    let workspace = state.workspace.get_workspace(workspace_id).await?;
978    state
979        .sync
980        .record_state_change(
981            workspace_id,
982            "full_sync",
983            payload.state.clone(),
984            workspace.version + 1,
985            auth_user.user_id,
986        )
987        .await?;
988
989    Ok(Json(serde_json::json!({
990        "workspace_id": workspace_id,
991        "version": workspace.version + 1,
992        "state": payload.state
993    })))
994}
995
996/// Get state change history
997async fn get_state_history(
998    State(state): State<ApiState>,
999    Path(workspace_id): Path<Uuid>,
1000    Extension(auth_user): Extension<AuthUser>,
1001    Query(params): Query<std::collections::HashMap<String, String>>,
1002) -> Result<Json<serde_json::Value>> {
1003    // Verify user is a member
1004    let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1005
1006    // Get since_version if specified
1007    let since_version =
1008        params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1009
1010    // Get state changes
1011    let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
1012
1013    Ok(Json(serde_json::json!({
1014        "workspace_id": workspace_id,
1015        "since_version": since_version,
1016        "changes": changes
1017    })))
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023
1024    #[test]
1025    fn test_router_creation() {
1026        // Just ensure router can be created
1027        use crate::events::EventBus;
1028        let event_bus = Arc::new(EventBus::new(100));
1029        let state = ApiState {
1030            auth: Arc::new(AuthService::new("test".to_string())),
1031            user: Arc::new(UserService::new(
1032                todo!(),
1033                Arc::new(AuthService::new("test".to_string())),
1034            )),
1035            workspace: Arc::new(WorkspaceService::new(todo!())),
1036            history: Arc::new(VersionControl::new(todo!())),
1037            merge: Arc::new(MergeService::new(todo!())),
1038            backup: Arc::new(BackupService::new(todo!(), None, todo!(), todo!())),
1039            sync: Arc::new(SyncEngine::new(event_bus)),
1040        };
1041        let _router = create_router(state);
1042    }
1043}