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),
161 Self::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg),
162 Self::WorkspaceNotFound(msg) => (StatusCode::NOT_FOUND, msg),
163 Self::UserNotFound(msg) => (StatusCode::NOT_FOUND, msg),
164 Self::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
165 Self::AlreadyExists(msg) => (StatusCode::CONFLICT, msg),
166 Self::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg),
167 _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()),
168 };
169
170 (status, Json(serde_json::json!({ "error": message }))).into_response()
171 }
172}
173
174async fn register(
178 State(state): State<ApiState>,
179 Json(payload): Json<RegisterRequest>,
180) -> Result<Json<AuthResponse>> {
181 let user = state
183 .user
184 .create_user(payload.username, payload.email, payload.password)
185 .await?;
186
187 let token = state.auth.generate_token(&user)?;
189
190 Ok(Json(AuthResponse {
191 access_token: token.access_token,
192 token_type: token.token_type,
193 expires_at: token.expires_at.to_rfc3339(),
194 }))
195}
196
197async fn login(
199 State(state): State<ApiState>,
200 Json(payload): Json<Credentials>,
201) -> Result<Json<AuthResponse>> {
202 let user = state.user.authenticate(&payload.username, &payload.password).await?;
204
205 let token = state.auth.generate_token(&user)?;
207
208 Ok(Json(AuthResponse {
209 access_token: token.access_token,
210 token_type: token.token_type,
211 expires_at: token.expires_at.to_rfc3339(),
212 }))
213}
214
215async fn create_workspace(
217 State(state): State<ApiState>,
218 Extension(auth_user): Extension<AuthUser>,
219 Json(payload): Json<CreateWorkspaceRequest>,
220) -> Result<Json<serde_json::Value>> {
221 let workspace = state
223 .workspace
224 .create_workspace(payload.name, payload.description, auth_user.user_id)
225 .await?;
226
227 Ok(Json(serde_json::to_value(workspace)?))
228}
229
230async fn list_workspaces(
232 State(state): State<ApiState>,
233 Extension(auth_user): Extension<AuthUser>,
234) -> Result<Json<serde_json::Value>> {
235 let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
237
238 Ok(Json(serde_json::to_value(workspaces)?))
239}
240
241async fn get_workspace(
243 State(state): State<ApiState>,
244 Path(id): Path<Uuid>,
245 Extension(auth_user): Extension<AuthUser>,
246) -> Result<Json<serde_json::Value>> {
247 let _member = state.workspace.get_member(id, auth_user.user_id).await?;
249
250 let workspace = state.workspace.get_workspace(id).await?;
252
253 Ok(Json(serde_json::to_value(workspace)?))
254}
255
256async fn update_workspace(
258 State(state): State<ApiState>,
259 Path(id): Path<Uuid>,
260 Extension(auth_user): Extension<AuthUser>,
261 Json(payload): Json<UpdateWorkspaceRequest>,
262) -> Result<Json<serde_json::Value>> {
263 let workspace = state
265 .workspace
266 .update_workspace(id, auth_user.user_id, payload.name, payload.description, None)
267 .await?;
268
269 Ok(Json(serde_json::to_value(workspace)?))
270}
271
272async fn delete_workspace(
274 State(state): State<ApiState>,
275 Path(id): Path<Uuid>,
276 Extension(auth_user): Extension<AuthUser>,
277) -> Result<StatusCode> {
278 state.workspace.delete_workspace(id, auth_user.user_id).await?;
280
281 Ok(StatusCode::NO_CONTENT)
282}
283
284async fn add_member(
286 State(state): State<ApiState>,
287 Path(workspace_id): Path<Uuid>,
288 Extension(auth_user): Extension<AuthUser>,
289 Json(payload): Json<AddMemberRequest>,
290) -> Result<Json<serde_json::Value>> {
291 let member = state
293 .workspace
294 .add_member(workspace_id, auth_user.user_id, payload.user_id, payload.role)
295 .await?;
296
297 Ok(Json(serde_json::to_value(member)?))
298}
299
300async fn remove_member(
302 State(state): State<ApiState>,
303 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
304 Extension(auth_user): Extension<AuthUser>,
305) -> Result<StatusCode> {
306 state
308 .workspace
309 .remove_member(workspace_id, auth_user.user_id, member_user_id)
310 .await?;
311
312 Ok(StatusCode::NO_CONTENT)
313}
314
315async fn change_role(
317 State(state): State<ApiState>,
318 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
319 Extension(auth_user): Extension<AuthUser>,
320 Json(payload): Json<ChangeRoleRequest>,
321) -> Result<Json<serde_json::Value>> {
322 let member = state
324 .workspace
325 .change_role(workspace_id, auth_user.user_id, member_user_id, payload.role)
326 .await?;
327
328 Ok(Json(serde_json::to_value(member)?))
329}
330
331async fn list_members(
333 State(state): State<ApiState>,
334 Path(workspace_id): Path<Uuid>,
335 Extension(auth_user): Extension<AuthUser>,
336) -> Result<Json<serde_json::Value>> {
337 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
339
340 let members = state.workspace.list_members(workspace_id).await?;
342
343 Ok(Json(serde_json::to_value(members)?))
344}
345
346async fn health_check() -> impl IntoResponse {
348 Json(serde_json::json!({
349 "status": "healthy",
350 "timestamp": chrono::Utc::now().to_rfc3339(),
351 }))
352}
353
354async fn readiness_check(State(state): State<ApiState>) -> impl IntoResponse {
356 let db_healthy = state.workspace.check_database_health().await;
360
361 if db_healthy {
362 Json(serde_json::json!({
363 "status": "ready",
364 "database": "healthy",
365 "timestamp": chrono::Utc::now().to_rfc3339(),
366 }))
367 .into_response()
368 } else {
369 (
370 StatusCode::SERVICE_UNAVAILABLE,
371 Json(serde_json::json!({
372 "status": "not_ready",
373 "database": "unhealthy",
374 "timestamp": chrono::Utc::now().to_rfc3339(),
375 })),
376 )
377 .into_response()
378 }
379}
380
381fn validate_commit_message(message: &str) -> Result<()> {
385 if message.is_empty() {
386 return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
387 }
388 if message.len() > 500 {
389 return Err(CollabError::InvalidInput(
390 "Commit message cannot exceed 500 characters".to_string(),
391 ));
392 }
393 Ok(())
394}
395
396fn validate_snapshot_name(name: &str) -> Result<()> {
398 if name.is_empty() {
399 return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
400 }
401 if name.len() > 100 {
402 return Err(CollabError::InvalidInput(
403 "Snapshot name cannot exceed 100 characters".to_string(),
404 ));
405 }
406 if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
408 return Err(CollabError::InvalidInput(
409 "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
410 ));
411 }
412 Ok(())
413}
414
415async fn create_commit(
448 State(state): State<ApiState>,
449 Path(workspace_id): Path<Uuid>,
450 Extension(auth_user): Extension<AuthUser>,
451 Json(payload): Json<CreateCommitRequest>,
452) -> Result<Json<serde_json::Value>> {
453 validate_commit_message(&payload.message)?;
455
456 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
458 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
459 return Err(CollabError::AuthorizationFailed(
460 "Only Admins and Editors can create commits".to_string(),
461 ));
462 }
463
464 let workspace = state.workspace.get_workspace(workspace_id).await?;
466
467 let parent = state.history.get_latest_commit(workspace_id).await?;
469 let parent_id = parent.as_ref().map(|c| c.id);
470 let version = parent.as_ref().map_or(1, |c| c.version + 1);
471
472 let snapshot = serde_json::to_value(&workspace)?;
474
475 let commit = state
477 .history
478 .create_commit(
479 workspace_id,
480 auth_user.user_id,
481 payload.message,
482 parent_id,
483 version,
484 snapshot,
485 payload.changes,
486 )
487 .await?;
488
489 Ok(Json(serde_json::to_value(commit)?))
490}
491
492async fn list_commits(
518 State(state): State<ApiState>,
519 Path(workspace_id): Path<Uuid>,
520 Extension(auth_user): Extension<AuthUser>,
521 Query(pagination): Query<PaginationQuery>,
522) -> Result<Json<serde_json::Value>> {
523 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
525
526 let limit = pagination.limit.clamp(1, 100);
528
529 let commits = state.history.get_history(workspace_id, Some(limit)).await?;
531
532 Ok(Json(serde_json::json!({
534 "commits": commits,
535 "pagination": {
536 "limit": limit,
537 "offset": pagination.offset,
538 }
539 })))
540}
541
542async fn get_commit(
556 State(state): State<ApiState>,
557 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
558 Extension(auth_user): Extension<AuthUser>,
559) -> Result<Json<serde_json::Value>> {
560 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
562
563 let commit = state.history.get_commit(commit_id).await?;
565
566 if commit.workspace_id != workspace_id {
568 return Err(CollabError::InvalidInput(
569 "Commit does not belong to this workspace".to_string(),
570 ));
571 }
572
573 Ok(Json(serde_json::to_value(commit)?))
574}
575
576async fn restore_to_commit(
596 State(state): State<ApiState>,
597 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
598 Extension(auth_user): Extension<AuthUser>,
599) -> Result<Json<serde_json::Value>> {
600 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
602 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
603 return Err(CollabError::AuthorizationFailed(
604 "Only Admins and Editors can restore workspaces".to_string(),
605 ));
606 }
607
608 let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
610
611 Ok(Json(serde_json::json!({
612 "workspace_id": workspace_id,
613 "commit_id": commit_id,
614 "restored_state": restored_state
615 })))
616}
617
618async fn create_snapshot(
639 State(state): State<ApiState>,
640 Path(workspace_id): Path<Uuid>,
641 Extension(auth_user): Extension<AuthUser>,
642 Json(payload): Json<CreateSnapshotRequest>,
643) -> Result<Json<serde_json::Value>> {
644 validate_snapshot_name(&payload.name)?;
646
647 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
649 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
650 return Err(CollabError::AuthorizationFailed(
651 "Only Admins and Editors can create snapshots".to_string(),
652 ));
653 }
654
655 let snapshot = state
657 .history
658 .create_snapshot(
659 workspace_id,
660 payload.name,
661 payload.description,
662 payload.commit_id,
663 auth_user.user_id,
664 )
665 .await?;
666
667 Ok(Json(serde_json::to_value(snapshot)?))
668}
669
670async fn list_snapshots(
681 State(state): State<ApiState>,
682 Path(workspace_id): Path<Uuid>,
683 Extension(auth_user): Extension<AuthUser>,
684) -> Result<Json<serde_json::Value>> {
685 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
687
688 let snapshots = state.history.list_snapshots(workspace_id).await?;
690
691 Ok(Json(serde_json::to_value(snapshots)?))
692}
693
694async fn get_snapshot(
705 State(state): State<ApiState>,
706 Path((workspace_id, name)): Path<(Uuid, String)>,
707 Extension(auth_user): Extension<AuthUser>,
708) -> Result<Json<serde_json::Value>> {
709 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
711
712 let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
714
715 Ok(Json(serde_json::to_value(snapshot)?))
716}
717
718#[derive(Debug, Deserialize)]
721pub struct ForkWorkspaceRequest {
722 pub name: Option<String>,
723 pub fork_point_commit_id: Option<Uuid>,
724}
725
726async fn fork_workspace(
728 State(state): State<ApiState>,
729 Path(workspace_id): Path<Uuid>,
730 Extension(auth_user): Extension<AuthUser>,
731 Json(payload): Json<ForkWorkspaceRequest>,
732) -> Result<Json<serde_json::Value>> {
733 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
735
736 let forked = state
738 .workspace
739 .fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
740 .await?;
741
742 Ok(Json(serde_json::to_value(forked)?))
743}
744
745async fn list_forks(
747 State(state): State<ApiState>,
748 Path(workspace_id): Path<Uuid>,
749 Extension(auth_user): Extension<AuthUser>,
750) -> Result<Json<serde_json::Value>> {
751 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
753
754 let forks = state.workspace.list_forks(workspace_id).await?;
756
757 Ok(Json(serde_json::to_value(forks)?))
758}
759
760#[derive(Debug, Deserialize)]
761pub struct MergeWorkspacesRequest {
762 pub source_workspace_id: Uuid,
763}
764
765async fn merge_workspaces(
767 State(state): State<ApiState>,
768 Path(target_workspace_id): Path<Uuid>,
769 Extension(auth_user): Extension<AuthUser>,
770 Json(payload): Json<MergeWorkspacesRequest>,
771) -> Result<Json<serde_json::Value>> {
772 let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
774 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
775 return Err(CollabError::AuthorizationFailed(
776 "Only Admins and Editors can merge workspaces".to_string(),
777 ));
778 }
779
780 let (merged_state, conflicts) = state
782 .merge
783 .merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
784 .await?;
785
786 Ok(Json(serde_json::json!({
787 "merged_state": merged_state,
788 "conflicts": conflicts,
789 "has_conflicts": !conflicts.is_empty()
790 })))
791}
792
793async fn list_merges(
795 State(state): State<ApiState>,
796 Path(workspace_id): Path<Uuid>,
797 Extension(auth_user): Extension<AuthUser>,
798) -> Result<Json<serde_json::Value>> {
799 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
801
802 let merges = state.merge.list_merges(workspace_id).await?;
804
805 Ok(Json(serde_json::to_value(merges)?))
806}
807
808#[derive(Debug, Deserialize)]
811pub struct CreateBackupRequest {
812 pub storage_backend: Option<String>,
813 pub format: Option<String>,
814 pub commit_id: Option<Uuid>,
815}
816
817async fn create_backup(
819 State(state): State<ApiState>,
820 Path(workspace_id): Path<Uuid>,
821 Extension(auth_user): Extension<AuthUser>,
822 Json(payload): Json<CreateBackupRequest>,
823) -> Result<Json<serde_json::Value>> {
824 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
826 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
827 return Err(CollabError::AuthorizationFailed(
828 "Only Admins and Editors can create backups".to_string(),
829 ));
830 }
831
832 let storage_backend = match payload.storage_backend.as_deref() {
834 Some("s3") => StorageBackend::S3,
835 Some("azure") => StorageBackend::Azure,
836 Some("gcs") => StorageBackend::Gcs,
837 Some("custom") => StorageBackend::Custom,
838 _ => StorageBackend::Local,
839 };
840
841 let backup = state
843 .backup
844 .backup_workspace(
845 workspace_id,
846 auth_user.user_id,
847 storage_backend,
848 payload.format,
849 payload.commit_id,
850 )
851 .await?;
852
853 Ok(Json(serde_json::to_value(backup)?))
854}
855
856async fn list_backups(
858 State(state): State<ApiState>,
859 Path(workspace_id): Path<Uuid>,
860 Extension(auth_user): Extension<AuthUser>,
861 Query(pagination): Query<PaginationQuery>,
862) -> Result<Json<serde_json::Value>> {
863 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
865
866 let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
868
869 Ok(Json(serde_json::to_value(backups)?))
870}
871
872async fn delete_backup(
874 State(state): State<ApiState>,
875 Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
876 Extension(auth_user): Extension<AuthUser>,
877) -> Result<StatusCode> {
878 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
880 if !matches!(member.role, UserRole::Admin) {
881 return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
882 }
883
884 state.backup.delete_backup(backup_id).await?;
886
887 Ok(StatusCode::NO_CONTENT)
888}
889
890#[derive(Debug, Deserialize)]
891pub struct RestoreWorkspaceRequest {
892 pub backup_id: Uuid,
893 pub target_workspace_id: Option<Uuid>,
894}
895
896async fn restore_workspace(
898 State(state): State<ApiState>,
899 Path(workspace_id): Path<Uuid>,
900 Extension(auth_user): Extension<AuthUser>,
901 Json(payload): Json<RestoreWorkspaceRequest>,
902) -> Result<Json<serde_json::Value>> {
903 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
905 if !matches!(member.role, UserRole::Admin) {
906 return Err(CollabError::AuthorizationFailed(
907 "Only Admins can restore workspaces".to_string(),
908 ));
909 }
910
911 let restored_id = state
913 .backup
914 .restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
915 .await?;
916
917 Ok(Json(serde_json::json!({
918 "workspace_id": restored_id,
919 "restored_from_backup": payload.backup_id
920 })))
921}
922
923async fn get_workspace_state(
927 State(state): State<ApiState>,
928 Path(workspace_id): Path<Uuid>,
929 Extension(auth_user): Extension<AuthUser>,
930 Query(params): Query<std::collections::HashMap<String, String>>,
931) -> Result<Json<serde_json::Value>> {
932 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
934
935 let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
937
938 let sync_state = if let Some(version) = version {
940 state.sync.load_state_snapshot(workspace_id, Some(version)).await?
941 } else {
942 if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
944 let workspace = state.workspace.get_workspace(workspace_id).await?;
946 return Ok(Json(serde_json::json!({
947 "workspace_id": workspace_id,
948 "version": workspace.version,
949 "state": full_state,
950 "last_updated": workspace.updated_at
951 })));
952 }
953
954 state.sync.get_state(workspace_id)
956 };
957
958 if let Some(state_val) = sync_state {
959 Ok(Json(serde_json::json!({
960 "workspace_id": workspace_id,
961 "version": state_val.version,
962 "state": state_val.state,
963 "last_updated": state_val.last_updated
964 })))
965 } else {
966 let workspace = state.workspace.get_workspace(workspace_id).await?;
968 Ok(Json(serde_json::json!({
969 "workspace_id": workspace_id,
970 "version": workspace.version,
971 "state": workspace.config,
972 "last_updated": workspace.updated_at
973 })))
974 }
975}
976
977#[derive(Debug, Deserialize)]
978pub struct UpdateWorkspaceStateRequest {
979 pub state: serde_json::Value,
980}
981
982async fn update_workspace_state(
984 State(state): State<ApiState>,
985 Path(workspace_id): Path<Uuid>,
986 Extension(auth_user): Extension<AuthUser>,
987 Json(payload): Json<UpdateWorkspaceStateRequest>,
988) -> Result<Json<serde_json::Value>> {
989 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
991 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
992 return Err(CollabError::AuthorizationFailed(
993 "Only Admins and Editors can update workspace state".to_string(),
994 ));
995 }
996
997 state.sync.update_state(workspace_id, payload.state.clone())?;
999
1000 let workspace = state.workspace.get_workspace(workspace_id).await?;
1002 state
1003 .sync
1004 .record_state_change(
1005 workspace_id,
1006 "full_sync",
1007 payload.state.clone(),
1008 workspace.version + 1,
1009 auth_user.user_id,
1010 )
1011 .await?;
1012
1013 Ok(Json(serde_json::json!({
1014 "workspace_id": workspace_id,
1015 "version": workspace.version + 1,
1016 "state": payload.state
1017 })))
1018}
1019
1020async fn get_state_history(
1022 State(state): State<ApiState>,
1023 Path(workspace_id): Path<Uuid>,
1024 Extension(auth_user): Extension<AuthUser>,
1025 Query(params): Query<std::collections::HashMap<String, String>>,
1026) -> Result<Json<serde_json::Value>> {
1027 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1029
1030 let since_version =
1032 params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1033
1034 let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
1036
1037 Ok(Json(serde_json::json!({
1038 "workspace_id": workspace_id,
1039 "since_version": since_version,
1040 "changes": changes
1041 })))
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 #[tokio::test]
1049 async fn test_router_creation() {
1050 use crate::core_bridge::CoreBridge;
1052 use crate::events::EventBus;
1053 use sqlx::SqlitePool;
1054 use tempfile::TempDir;
1055
1056 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1058 let workspace_dir = temp_dir.path().join("workspaces");
1059 let backup_dir = temp_dir.path().join("backups");
1060 std::fs::create_dir_all(&workspace_dir).expect("Failed to create workspace dir");
1061 std::fs::create_dir_all(&backup_dir).expect("Failed to create backup dir");
1062
1063 let db = SqlitePool::connect("sqlite::memory:")
1065 .await
1066 .expect("Failed to create database pool");
1067
1068 sqlx::migrate!("./migrations").run(&db).await.expect("Failed to run migrations");
1070
1071 let core_bridge = Arc::new(CoreBridge::new(&workspace_dir));
1073
1074 let auth = Arc::new(AuthService::new("test-secret-key".to_string()));
1076 let user = Arc::new(UserService::new(db.clone(), auth.clone()));
1077 let workspace =
1078 Arc::new(WorkspaceService::with_core_bridge(db.clone(), core_bridge.clone()));
1079 let history = Arc::new(VersionControl::new(db.clone()));
1080 let merge = Arc::new(MergeService::new(db.clone()));
1081 let backup = Arc::new(BackupService::new(
1082 db.clone(),
1083 Some(backup_dir.to_string_lossy().to_string()),
1084 core_bridge.clone(),
1085 workspace.clone(),
1086 ));
1087 let event_bus = Arc::new(EventBus::new(100));
1088 let sync = Arc::new(SyncEngine::new(event_bus));
1089
1090 let state = ApiState {
1091 auth,
1092 user,
1093 workspace,
1094 history,
1095 merge,
1096 backup,
1097 sync,
1098 };
1099 let _router = create_router(state);
1100 }
1101}