1use 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#[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
37pub fn create_router(state: ApiState) -> Router {
39 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 let protected_routes = Router::new()
48 .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 .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 .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 .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 .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 .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 .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 Router::new().merge(public_routes).merge(protected_routes).with_state(state)
89}
90
91#[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
155impl 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
184async fn register(
188 State(state): State<ApiState>,
189 Json(payload): Json<RegisterRequest>,
190) -> Result<Json<AuthResponse>> {
191 let user = state
193 .user
194 .create_user(payload.username, payload.email, payload.password)
195 .await?;
196
197 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
207async fn login(
209 State(state): State<ApiState>,
210 Json(payload): Json<Credentials>,
211) -> Result<Json<AuthResponse>> {
212 let user = state.user.authenticate(&payload.username, &payload.password).await?;
214
215 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
225async 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 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
240async fn list_workspaces(
242 State(state): State<ApiState>,
243 Extension(auth_user): Extension<AuthUser>,
244) -> Result<Json<serde_json::Value>> {
245 let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
247
248 Ok(Json(serde_json::to_value(workspaces)?))
249}
250
251async 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 let _member = state.workspace.get_member(id, auth_user.user_id).await?;
259
260 let workspace = state.workspace.get_workspace(id).await?;
262
263 Ok(Json(serde_json::to_value(workspace)?))
264}
265
266async 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 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
282async fn delete_workspace(
284 State(state): State<ApiState>,
285 Path(id): Path<Uuid>,
286 Extension(auth_user): Extension<AuthUser>,
287) -> Result<StatusCode> {
288 state.workspace.delete_workspace(id, auth_user.user_id).await?;
290
291 Ok(StatusCode::NO_CONTENT)
292}
293
294async 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 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
310async 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 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
325async 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 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
341async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
349
350 let members = state.workspace.list_members(workspace_id).await?;
352
353 Ok(Json(serde_json::to_value(members)?))
354}
355
356async fn health_check() -> impl IntoResponse {
358 Json(serde_json::json!({
359 "status": "healthy",
360 "timestamp": chrono::Utc::now().to_rfc3339(),
361 }))
362}
363
364async fn readiness_check(State(state): State<ApiState>) -> impl IntoResponse {
366 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
391fn 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
406fn 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 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
425async 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_commit_message(&payload.message)?;
465
466 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 let workspace = state.workspace.get_workspace(workspace_id).await?;
476
477 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 let snapshot = serde_json::to_value(&workspace)?;
484
485 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
502async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
535
536 let limit = pagination.limit.clamp(1, 100);
538
539 let commits = state.history.get_history(workspace_id, Some(limit)).await?;
541
542 Ok(Json(serde_json::json!({
544 "commits": commits,
545 "pagination": {
546 "limit": limit,
547 "offset": pagination.offset,
548 }
549 })))
550}
551
552async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
572
573 let commit = state.history.get_commit(commit_id).await?;
575
576 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
586async 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 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 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
628async 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_snapshot_name(&payload.name)?;
656
657 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 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
680async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
697
698 let snapshots = state.history.list_snapshots(workspace_id).await?;
700
701 Ok(Json(serde_json::to_value(snapshots)?))
702}
703
704async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
721
722 let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
724
725 Ok(Json(serde_json::to_value(snapshot)?))
726}
727
728#[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
737async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
746
747 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
756async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
764
765 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
776async 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 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 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
804async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
812
813 let merges = state.merge.list_merges(workspace_id).await?;
815
816 Ok(Json(serde_json::to_value(merges)?))
817}
818
819#[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
828async 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 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 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 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
867async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
876
877 let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
879
880 Ok(Json(serde_json::to_value(backups)?))
881}
882
883async 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 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 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
907async 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 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 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
934async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
945
946 let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
948
949 let sync_state = if let Some(version) = version {
951 state.sync.load_state_snapshot(workspace_id, Some(version)).await?
952 } else {
953 if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
955 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 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 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
993async 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 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 state.sync.update_state(workspace_id, payload.state.clone())?;
1010
1011 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
1031async 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 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1040
1041 let since_version =
1043 params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1044
1045 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 use crate::core_bridge::CoreBridge;
1063 use crate::events::EventBus;
1064 use sqlx::SqlitePool;
1065 use tempfile::TempDir;
1066
1067 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 let db = SqlitePool::connect("sqlite::memory:")
1076 .await
1077 .expect("Failed to create database pool");
1078
1079 sqlx::migrate!("./migrations").run(&db).await.expect("Failed to run migrations");
1081
1082 let core_bridge = Arc::new(CoreBridge::new(&workspace_dir));
1084
1085 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}