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