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>,
30 pub user: Arc<UserService>,
32 pub workspace: Arc<WorkspaceService>,
34 pub history: Arc<VersionControl>,
36 pub merge: Arc<MergeService>,
38 pub backup: Arc<BackupService>,
40 pub sync: Arc<SyncEngine>,
42}
43
44pub fn create_router(state: ApiState) -> Router {
46 let public_routes = Router::new()
48 .route("/auth/register", post(register))
49 .route("/auth/login", post(login))
50 .route("/health", get(health_check))
51 .route("/ready", get(readiness_check));
52
53 let protected_routes = Router::new()
55 .route("/workspaces", post(create_workspace))
57 .route("/workspaces", get(list_workspaces))
58 .route("/workspaces/{id}", get(get_workspace))
59 .route("/workspaces/{id}", put(update_workspace))
60 .route("/workspaces/{id}", delete(delete_workspace))
61 .route("/workspaces/{id}/members", post(add_member))
63 .route("/workspaces/{id}/members/{user_id}", delete(remove_member))
64 .route("/workspaces/{id}/members/{user_id}/role", put(change_role))
65 .route("/workspaces/{id}/members", get(list_members))
66 .route("/workspaces/{id}/commits", post(create_commit))
68 .route("/workspaces/{id}/commits", get(list_commits))
69 .route("/workspaces/{id}/commits/{commit_id}", get(get_commit))
70 .route("/workspaces/{id}/restore/{commit_id}", post(restore_to_commit))
71 .route("/workspaces/{id}/snapshots", post(create_snapshot))
73 .route("/workspaces/{id}/snapshots", get(list_snapshots))
74 .route("/workspaces/{id}/snapshots/{name}", get(get_snapshot))
75 .route("/workspaces/{id}/fork", post(fork_workspace))
77 .route("/workspaces/{id}/forks", get(list_forks))
78 .route("/workspaces/{id}/merge", post(merge_workspaces))
79 .route("/workspaces/{id}/merges", get(list_merges))
80 .route("/workspaces/{id}/backup", post(create_backup))
82 .route("/workspaces/{id}/backups", get(list_backups))
83 .route("/workspaces/{id}/backups/{backup_id}", delete(delete_backup))
84 .route("/workspaces/{id}/restore", post(restore_workspace))
85 .route("/workspaces/{id}/state", get(get_workspace_state))
87 .route("/workspaces/{id}/state", post(update_workspace_state))
88 .route("/workspaces/{id}/state/history", get(get_state_history))
89 .route_layer(middleware::from_fn_with_state(
90 state.auth.clone(),
91 auth_middleware,
92 ));
93
94 Router::new().merge(public_routes).merge(protected_routes).with_state(state)
96}
97
98#[derive(Debug, Deserialize)]
102pub struct RegisterRequest {
103 pub username: String,
105 pub email: String,
107 pub password: String,
109}
110
111#[derive(Debug, Serialize)]
113pub struct AuthResponse {
114 pub access_token: String,
116 pub token_type: String,
118 pub expires_at: String,
120}
121
122#[derive(Debug, Deserialize)]
124pub struct CreateWorkspaceRequest {
125 pub name: String,
127 pub description: Option<String>,
129}
130
131#[derive(Debug, Deserialize)]
133pub struct UpdateWorkspaceRequest {
134 pub name: Option<String>,
136 pub description: Option<String>,
138}
139
140#[derive(Debug, Deserialize)]
142pub struct AddMemberRequest {
143 pub user_id: Uuid,
145 pub role: UserRole,
147}
148
149#[derive(Debug, Deserialize)]
151pub struct ChangeRoleRequest {
152 pub role: UserRole,
154}
155
156#[derive(Debug, Deserialize)]
158pub struct CreateCommitRequest {
159 pub message: String,
161 pub changes: serde_json::Value,
163}
164
165#[derive(Debug, Deserialize)]
167pub struct CreateSnapshotRequest {
168 pub name: String,
170 pub description: Option<String>,
172 pub commit_id: Uuid,
174}
175
176#[derive(Debug, Deserialize)]
178pub struct PaginationQuery {
179 #[serde(default = "default_limit")]
181 pub limit: i32,
182 #[serde(default)]
184 pub offset: i32,
185}
186
187const fn default_limit() -> i32 {
188 50
189}
190
191impl IntoResponse for CollabError {
194 fn into_response(self) -> Response {
195 let (status, message) = match &self {
196 Self::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
197 Self::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg.clone()),
198 Self::WorkspaceNotFound(msg) | Self::UserNotFound(msg) => {
199 (StatusCode::NOT_FOUND, msg.clone())
200 }
201 Self::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
202 Self::AlreadyExists(msg) | Self::ConflictDetected(msg) => {
203 (StatusCode::CONFLICT, msg.clone())
204 }
205 Self::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg.clone()),
206 Self::Internal(msg)
207 | Self::DatabaseError(msg)
208 | Self::SerializationError(msg)
209 | Self::SyncError(msg)
210 | Self::WebSocketError(msg)
211 | Self::ConnectionError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
212 Self::VersionMismatch { expected, actual } => (
213 StatusCode::CONFLICT,
214 format!("Version mismatch: expected {expected}, got {actual}"),
215 ),
216 };
217
218 (status, Json(serde_json::json!({ "error": message }))).into_response()
219 }
220}
221
222async fn register(
226 State(state): State<ApiState>,
227 Json(payload): Json<RegisterRequest>,
228) -> Result<Json<AuthResponse>> {
229 let user = state
231 .user
232 .create_user(payload.username, payload.email, payload.password)
233 .await?;
234
235 let token = state.auth.generate_token(&user)?;
237
238 Ok(Json(AuthResponse {
239 access_token: token.access_token,
240 token_type: token.token_type,
241 expires_at: token.expires_at.to_rfc3339(),
242 }))
243}
244
245async fn login(
247 State(state): State<ApiState>,
248 Json(payload): Json<Credentials>,
249) -> Result<Json<AuthResponse>> {
250 let user = state.user.authenticate(&payload.username, &payload.password).await?;
252
253 let token = state.auth.generate_token(&user)?;
255
256 Ok(Json(AuthResponse {
257 access_token: token.access_token,
258 token_type: token.token_type,
259 expires_at: token.expires_at.to_rfc3339(),
260 }))
261}
262
263async fn create_workspace(
265 State(state): State<ApiState>,
266 Extension(auth_user): Extension<AuthUser>,
267 Json(payload): Json<CreateWorkspaceRequest>,
268) -> Result<Json<serde_json::Value>> {
269 let workspace = state
271 .workspace
272 .create_workspace(payload.name, payload.description, auth_user.user_id)
273 .await?;
274
275 Ok(Json(serde_json::to_value(workspace)?))
276}
277
278async fn list_workspaces(
280 State(state): State<ApiState>,
281 Extension(auth_user): Extension<AuthUser>,
282) -> Result<Json<serde_json::Value>> {
283 let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
285
286 Ok(Json(serde_json::to_value(workspaces)?))
287}
288
289async fn get_workspace(
291 State(state): State<ApiState>,
292 Path(id): Path<Uuid>,
293 Extension(auth_user): Extension<AuthUser>,
294) -> Result<Json<serde_json::Value>> {
295 let _member = state.workspace.get_member(id, auth_user.user_id).await?;
297
298 let workspace = state.workspace.get_workspace(id).await?;
300
301 Ok(Json(serde_json::to_value(workspace)?))
302}
303
304async fn update_workspace(
306 State(state): State<ApiState>,
307 Path(id): Path<Uuid>,
308 Extension(auth_user): Extension<AuthUser>,
309 Json(payload): Json<UpdateWorkspaceRequest>,
310) -> Result<Json<serde_json::Value>> {
311 let workspace = state
313 .workspace
314 .update_workspace(id, auth_user.user_id, payload.name, payload.description, None)
315 .await?;
316
317 Ok(Json(serde_json::to_value(workspace)?))
318}
319
320async fn delete_workspace(
322 State(state): State<ApiState>,
323 Path(id): Path<Uuid>,
324 Extension(auth_user): Extension<AuthUser>,
325) -> Result<StatusCode> {
326 state.workspace.delete_workspace(id, auth_user.user_id).await?;
328
329 Ok(StatusCode::NO_CONTENT)
330}
331
332async fn add_member(
334 State(state): State<ApiState>,
335 Path(workspace_id): Path<Uuid>,
336 Extension(auth_user): Extension<AuthUser>,
337 Json(payload): Json<AddMemberRequest>,
338) -> Result<Json<serde_json::Value>> {
339 let member = state
341 .workspace
342 .add_member(workspace_id, auth_user.user_id, payload.user_id, payload.role)
343 .await?;
344
345 Ok(Json(serde_json::to_value(member)?))
346}
347
348async fn remove_member(
350 State(state): State<ApiState>,
351 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
352 Extension(auth_user): Extension<AuthUser>,
353) -> Result<StatusCode> {
354 state
356 .workspace
357 .remove_member(workspace_id, auth_user.user_id, member_user_id)
358 .await?;
359
360 Ok(StatusCode::NO_CONTENT)
361}
362
363async fn change_role(
365 State(state): State<ApiState>,
366 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
367 Extension(auth_user): Extension<AuthUser>,
368 Json(payload): Json<ChangeRoleRequest>,
369) -> Result<Json<serde_json::Value>> {
370 let member = state
372 .workspace
373 .change_role(workspace_id, auth_user.user_id, member_user_id, payload.role)
374 .await?;
375
376 Ok(Json(serde_json::to_value(member)?))
377}
378
379async fn list_members(
381 State(state): State<ApiState>,
382 Path(workspace_id): Path<Uuid>,
383 Extension(auth_user): Extension<AuthUser>,
384) -> Result<Json<serde_json::Value>> {
385 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
387
388 let members = state.workspace.list_members(workspace_id).await?;
390
391 Ok(Json(serde_json::to_value(members)?))
392}
393
394async fn health_check() -> impl IntoResponse {
396 Json(serde_json::json!({
397 "status": "healthy",
398 "timestamp": chrono::Utc::now().to_rfc3339(),
399 }))
400}
401
402async fn readiness_check(State(state): State<ApiState>) -> impl IntoResponse {
404 let db_healthy = state.workspace.check_database_health().await;
408
409 if db_healthy {
410 Json(serde_json::json!({
411 "status": "ready",
412 "database": "healthy",
413 "timestamp": chrono::Utc::now().to_rfc3339(),
414 }))
415 .into_response()
416 } else {
417 (
418 StatusCode::SERVICE_UNAVAILABLE,
419 Json(serde_json::json!({
420 "status": "not_ready",
421 "database": "unhealthy",
422 "timestamp": chrono::Utc::now().to_rfc3339(),
423 })),
424 )
425 .into_response()
426 }
427}
428
429fn validate_commit_message(message: &str) -> Result<()> {
433 if message.is_empty() {
434 return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
435 }
436 if message.len() > 500 {
437 return Err(CollabError::InvalidInput(
438 "Commit message cannot exceed 500 characters".to_string(),
439 ));
440 }
441 Ok(())
442}
443
444fn validate_snapshot_name(name: &str) -> Result<()> {
446 if name.is_empty() {
447 return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
448 }
449 if name.len() > 100 {
450 return Err(CollabError::InvalidInput(
451 "Snapshot name cannot exceed 100 characters".to_string(),
452 ));
453 }
454 if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
456 return Err(CollabError::InvalidInput(
457 "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
458 ));
459 }
460 Ok(())
461}
462
463async fn create_commit(
496 State(state): State<ApiState>,
497 Path(workspace_id): Path<Uuid>,
498 Extension(auth_user): Extension<AuthUser>,
499 Json(payload): Json<CreateCommitRequest>,
500) -> Result<Json<serde_json::Value>> {
501 validate_commit_message(&payload.message)?;
503
504 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
506 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
507 return Err(CollabError::AuthorizationFailed(
508 "Only Admins and Editors can create commits".to_string(),
509 ));
510 }
511
512 let workspace = state.workspace.get_workspace(workspace_id).await?;
514
515 let parent = state.history.get_latest_commit(workspace_id).await?;
517 let parent_id = parent.as_ref().map(|c| c.id);
518 let version = parent.as_ref().map_or(1, |c| c.version + 1);
519
520 let snapshot = serde_json::to_value(&workspace)?;
522
523 let commit = state
525 .history
526 .create_commit(
527 workspace_id,
528 auth_user.user_id,
529 payload.message,
530 parent_id,
531 version,
532 snapshot,
533 payload.changes,
534 )
535 .await?;
536
537 Ok(Json(serde_json::to_value(commit)?))
538}
539
540async fn list_commits(
566 State(state): State<ApiState>,
567 Path(workspace_id): Path<Uuid>,
568 Extension(auth_user): Extension<AuthUser>,
569 Query(pagination): Query<PaginationQuery>,
570) -> Result<Json<serde_json::Value>> {
571 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
573
574 let limit = pagination.limit.clamp(1, 100);
576
577 let commits = state.history.get_history(workspace_id, Some(limit)).await?;
579
580 Ok(Json(serde_json::json!({
582 "commits": commits,
583 "pagination": {
584 "limit": limit,
585 "offset": pagination.offset,
586 }
587 })))
588}
589
590async fn get_commit(
604 State(state): State<ApiState>,
605 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
606 Extension(auth_user): Extension<AuthUser>,
607) -> Result<Json<serde_json::Value>> {
608 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
610
611 let commit = state.history.get_commit(commit_id).await?;
613
614 if commit.workspace_id != workspace_id {
616 return Err(CollabError::InvalidInput(
617 "Commit does not belong to this workspace".to_string(),
618 ));
619 }
620
621 Ok(Json(serde_json::to_value(commit)?))
622}
623
624async fn restore_to_commit(
644 State(state): State<ApiState>,
645 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
646 Extension(auth_user): Extension<AuthUser>,
647) -> Result<Json<serde_json::Value>> {
648 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
650 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
651 return Err(CollabError::AuthorizationFailed(
652 "Only Admins and Editors can restore workspaces".to_string(),
653 ));
654 }
655
656 let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
658
659 Ok(Json(serde_json::json!({
660 "workspace_id": workspace_id,
661 "commit_id": commit_id,
662 "restored_state": restored_state
663 })))
664}
665
666async fn create_snapshot(
687 State(state): State<ApiState>,
688 Path(workspace_id): Path<Uuid>,
689 Extension(auth_user): Extension<AuthUser>,
690 Json(payload): Json<CreateSnapshotRequest>,
691) -> Result<Json<serde_json::Value>> {
692 validate_snapshot_name(&payload.name)?;
694
695 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
697 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
698 return Err(CollabError::AuthorizationFailed(
699 "Only Admins and Editors can create snapshots".to_string(),
700 ));
701 }
702
703 let snapshot = state
705 .history
706 .create_snapshot(
707 workspace_id,
708 payload.name,
709 payload.description,
710 payload.commit_id,
711 auth_user.user_id,
712 )
713 .await?;
714
715 Ok(Json(serde_json::to_value(snapshot)?))
716}
717
718async fn list_snapshots(
729 State(state): State<ApiState>,
730 Path(workspace_id): Path<Uuid>,
731 Extension(auth_user): Extension<AuthUser>,
732) -> Result<Json<serde_json::Value>> {
733 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
735
736 let snapshots = state.history.list_snapshots(workspace_id).await?;
738
739 Ok(Json(serde_json::to_value(snapshots)?))
740}
741
742async fn get_snapshot(
753 State(state): State<ApiState>,
754 Path((workspace_id, name)): Path<(Uuid, String)>,
755 Extension(auth_user): Extension<AuthUser>,
756) -> Result<Json<serde_json::Value>> {
757 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
759
760 let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
762
763 Ok(Json(serde_json::to_value(snapshot)?))
764}
765
766#[derive(Debug, Deserialize)]
770pub struct ForkWorkspaceRequest {
771 #[serde(alias = "new_name")]
773 pub name: Option<String>,
774 pub fork_point_commit_id: Option<Uuid>,
776}
777
778async fn fork_workspace(
780 State(state): State<ApiState>,
781 Path(workspace_id): Path<Uuid>,
782 Extension(auth_user): Extension<AuthUser>,
783 Json(payload): Json<ForkWorkspaceRequest>,
784) -> Result<Json<serde_json::Value>> {
785 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
787
788 let forked = state
790 .workspace
791 .fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
792 .await?;
793
794 Ok(Json(serde_json::to_value(forked)?))
795}
796
797async fn list_forks(
799 State(state): State<ApiState>,
800 Path(workspace_id): Path<Uuid>,
801 Extension(auth_user): Extension<AuthUser>,
802) -> Result<Json<serde_json::Value>> {
803 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
805
806 let forks = state.workspace.list_forks(workspace_id).await?;
808
809 Ok(Json(serde_json::to_value(forks)?))
810}
811
812#[derive(Debug, Deserialize)]
814pub struct MergeWorkspacesRequest {
815 pub source_workspace_id: Uuid,
817}
818
819async fn merge_workspaces(
821 State(state): State<ApiState>,
822 Path(target_workspace_id): Path<Uuid>,
823 Extension(auth_user): Extension<AuthUser>,
824 Json(payload): Json<MergeWorkspacesRequest>,
825) -> Result<Json<serde_json::Value>> {
826 let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
828 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
829 return Err(CollabError::AuthorizationFailed(
830 "Only Admins and Editors can merge workspaces".to_string(),
831 ));
832 }
833
834 let (merged_state, conflicts) = state
836 .merge
837 .merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
838 .await?;
839
840 Ok(Json(serde_json::json!({
841 "workspace": merged_state,
842 "conflicts": conflicts,
843 "has_conflicts": !conflicts.is_empty()
844 })))
845}
846
847async fn list_merges(
849 State(state): State<ApiState>,
850 Path(workspace_id): Path<Uuid>,
851 Extension(auth_user): Extension<AuthUser>,
852) -> Result<Json<serde_json::Value>> {
853 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
855
856 let merges = state.merge.list_merges(workspace_id).await?;
858
859 Ok(Json(serde_json::to_value(merges)?))
860}
861
862#[derive(Debug, Deserialize)]
866pub struct CreateBackupRequest {
867 pub storage_backend: Option<String>,
869 pub format: Option<String>,
871 pub commit_id: Option<Uuid>,
873}
874
875#[allow(clippy::large_futures)]
877async fn create_backup(
878 State(state): State<ApiState>,
879 Path(workspace_id): Path<Uuid>,
880 Extension(auth_user): Extension<AuthUser>,
881 Json(payload): Json<CreateBackupRequest>,
882) -> Result<Json<serde_json::Value>> {
883 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
885 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
886 return Err(CollabError::AuthorizationFailed(
887 "Only Admins and Editors can create backups".to_string(),
888 ));
889 }
890
891 let storage_backend = match payload.storage_backend.as_deref() {
893 Some("s3") => StorageBackend::S3,
894 Some("azure") => StorageBackend::Azure,
895 Some("gcs") => StorageBackend::Gcs,
896 Some("custom") => StorageBackend::Custom,
897 _ => StorageBackend::Local,
898 };
899
900 let backup = state
902 .backup
903 .backup_workspace(
904 workspace_id,
905 auth_user.user_id,
906 storage_backend,
907 payload.format,
908 payload.commit_id,
909 )
910 .await?;
911
912 Ok(Json(serde_json::to_value(backup)?))
913}
914
915async fn list_backups(
917 State(state): State<ApiState>,
918 Path(workspace_id): Path<Uuid>,
919 Extension(auth_user): Extension<AuthUser>,
920 Query(pagination): Query<PaginationQuery>,
921) -> Result<Json<serde_json::Value>> {
922 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
924
925 let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
927
928 Ok(Json(serde_json::to_value(backups)?))
929}
930
931async fn delete_backup(
933 State(state): State<ApiState>,
934 Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
935 Extension(auth_user): Extension<AuthUser>,
936) -> Result<StatusCode> {
937 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
939 if !matches!(member.role, UserRole::Admin) {
940 return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
941 }
942
943 state.backup.delete_backup(backup_id).await?;
945
946 Ok(StatusCode::NO_CONTENT)
947}
948
949#[derive(Debug, Deserialize)]
951pub struct RestoreWorkspaceRequest {
952 pub backup_id: Uuid,
954 pub target_workspace_id: Option<Uuid>,
956}
957
958async fn restore_workspace(
960 State(state): State<ApiState>,
961 Path(workspace_id): Path<Uuid>,
962 Extension(auth_user): Extension<AuthUser>,
963 Json(payload): Json<RestoreWorkspaceRequest>,
964) -> Result<Json<serde_json::Value>> {
965 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
967 if !matches!(member.role, UserRole::Admin) {
968 return Err(CollabError::AuthorizationFailed(
969 "Only Admins can restore workspaces".to_string(),
970 ));
971 }
972
973 let restored_id = state
975 .backup
976 .restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
977 .await?;
978
979 Ok(Json(serde_json::json!({
980 "workspace_id": restored_id,
981 "restored_from_backup": payload.backup_id
982 })))
983}
984
985async fn get_workspace_state(
989 State(state): State<ApiState>,
990 Path(workspace_id): Path<Uuid>,
991 Extension(auth_user): Extension<AuthUser>,
992 Query(params): Query<std::collections::HashMap<String, String>>,
993) -> Result<Json<serde_json::Value>> {
994 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
996
997 let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
999
1000 let sync_state = if let Some(version) = version {
1002 state.sync.load_state_snapshot(workspace_id, Some(version)).await?
1003 } else {
1004 if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
1006 let workspace = state.workspace.get_workspace(workspace_id).await?;
1008 return Ok(Json(serde_json::json!({
1009 "workspace_id": workspace_id,
1010 "version": workspace.version,
1011 "state": full_state,
1012 "last_updated": workspace.updated_at
1013 })));
1014 }
1015
1016 state.sync.get_state(workspace_id)
1018 };
1019
1020 if let Some(state_val) = sync_state {
1021 Ok(Json(serde_json::json!({
1022 "workspace_id": workspace_id,
1023 "version": state_val.version,
1024 "state": state_val.state,
1025 "last_updated": state_val.last_updated
1026 })))
1027 } else {
1028 let workspace = state.workspace.get_workspace(workspace_id).await?;
1030 Ok(Json(serde_json::json!({
1031 "workspace_id": workspace_id,
1032 "version": workspace.version,
1033 "state": workspace.config,
1034 "last_updated": workspace.updated_at
1035 })))
1036 }
1037}
1038
1039#[derive(Debug, Deserialize)]
1041pub struct UpdateWorkspaceStateRequest {
1042 pub state: serde_json::Value,
1044}
1045
1046async fn update_workspace_state(
1048 State(state): State<ApiState>,
1049 Path(workspace_id): Path<Uuid>,
1050 Extension(auth_user): Extension<AuthUser>,
1051 Json(payload): Json<UpdateWorkspaceStateRequest>,
1052) -> Result<Json<serde_json::Value>> {
1053 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1055 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
1056 return Err(CollabError::AuthorizationFailed(
1057 "Only Admins and Editors can update workspace state".to_string(),
1058 ));
1059 }
1060
1061 state.sync.update_state(workspace_id, payload.state.clone())?;
1063
1064 let workspace = state.workspace.get_workspace(workspace_id).await?;
1066 state
1067 .sync
1068 .record_state_change(
1069 workspace_id,
1070 "full_sync",
1071 payload.state.clone(),
1072 workspace.version + 1,
1073 auth_user.user_id,
1074 )
1075 .await?;
1076
1077 Ok(Json(serde_json::json!({
1078 "workspace_id": workspace_id,
1079 "version": workspace.version + 1,
1080 "state": payload.state
1081 })))
1082}
1083
1084async fn get_state_history(
1086 State(state): State<ApiState>,
1087 Path(workspace_id): Path<Uuid>,
1088 Extension(auth_user): Extension<AuthUser>,
1089 Query(params): Query<std::collections::HashMap<String, String>>,
1090) -> Result<Json<serde_json::Value>> {
1091 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1093
1094 let since_version =
1096 params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1097
1098 let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
1100
1101 Ok(Json(serde_json::json!({
1102 "workspace_id": workspace_id,
1103 "since_version": since_version,
1104 "changes": changes
1105 })))
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 #[tokio::test]
1111 async fn test_router_creation() {
1112 use super::*;
1114 use crate::core_bridge::CoreBridge;
1115 use crate::events::EventBus;
1116 use sqlx::SqlitePool;
1117 use tempfile::TempDir;
1118
1119 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1121 let workspace_dir = temp_dir.path().join("workspaces");
1122 let backup_dir = temp_dir.path().join("backups");
1123 std::fs::create_dir_all(&workspace_dir).expect("Failed to create workspace dir");
1124 std::fs::create_dir_all(&backup_dir).expect("Failed to create backup dir");
1125
1126 let db = SqlitePool::connect("sqlite::memory:")
1128 .await
1129 .expect("Failed to create database pool");
1130
1131 sqlx::migrate!("./migrations").run(&db).await.expect("Failed to run migrations");
1133
1134 let core_bridge = Arc::new(CoreBridge::new(&workspace_dir));
1136
1137 let auth = Arc::new(AuthService::new("test-secret-key".to_string()));
1139 let user = Arc::new(UserService::new(db.clone(), auth.clone()));
1140 let workspace =
1141 Arc::new(WorkspaceService::with_core_bridge(db.clone(), core_bridge.clone()));
1142 let history = Arc::new(VersionControl::new(db.clone()));
1143 let merge = Arc::new(MergeService::new(db.clone()));
1144 let backup = Arc::new(BackupService::new(
1145 db.clone(),
1146 Some(backup_dir.to_string_lossy().to_string()),
1147 core_bridge,
1148 workspace.clone(),
1149 ));
1150 let event_bus = Arc::new(EventBus::new(100));
1151 let sync = Arc::new(SyncEngine::new(event_bus));
1152
1153 let state = ApiState {
1154 auth,
1155 user,
1156 workspace,
1157 history,
1158 merge,
1159 backup,
1160 sync,
1161 };
1162 let _router = create_router(state);
1163 }
1164}