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
151fn default_limit() -> i32 {
152 50
153}
154
155impl IntoResponse for CollabError {
158 fn into_response(self) -> Response {
159 let (status, message) = match self {
160 CollabError::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg),
161 CollabError::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg),
162 CollabError::WorkspaceNotFound(msg) => (StatusCode::NOT_FOUND, msg),
163 CollabError::UserNotFound(msg) => (StatusCode::NOT_FOUND, msg),
164 CollabError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
165 CollabError::AlreadyExists(msg) => (StatusCode::CONFLICT, msg),
166 CollabError::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg),
167 _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()),
168 };
169
170 (status, Json(serde_json::json!({ "error": message }))).into_response()
171 }
172}
173
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 StatusCode::OK
349}
350
351async fn readiness_check() -> impl IntoResponse {
353 StatusCode::OK
355}
356
357fn validate_commit_message(message: &str) -> Result<()> {
361 if message.is_empty() {
362 return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
363 }
364 if message.len() > 500 {
365 return Err(CollabError::InvalidInput(
366 "Commit message cannot exceed 500 characters".to_string(),
367 ));
368 }
369 Ok(())
370}
371
372fn validate_snapshot_name(name: &str) -> Result<()> {
374 if name.is_empty() {
375 return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
376 }
377 if name.len() > 100 {
378 return Err(CollabError::InvalidInput(
379 "Snapshot name cannot exceed 100 characters".to_string(),
380 ));
381 }
382 if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
384 return Err(CollabError::InvalidInput(
385 "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
386 ));
387 }
388 Ok(())
389}
390
391async fn create_commit(
424 State(state): State<ApiState>,
425 Path(workspace_id): Path<Uuid>,
426 Extension(auth_user): Extension<AuthUser>,
427 Json(payload): Json<CreateCommitRequest>,
428) -> Result<Json<serde_json::Value>> {
429 validate_commit_message(&payload.message)?;
431
432 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
434 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
435 return Err(CollabError::AuthorizationFailed(
436 "Only Admins and Editors can create commits".to_string(),
437 ));
438 }
439
440 let workspace = state.workspace.get_workspace(workspace_id).await?;
442
443 let parent = state.history.get_latest_commit(workspace_id).await?;
445 let parent_id = parent.as_ref().map(|c| c.id);
446 let version = parent.as_ref().map(|c| c.version + 1).unwrap_or(1);
447
448 let snapshot = serde_json::to_value(&workspace)?;
450
451 let commit = state
453 .history
454 .create_commit(
455 workspace_id,
456 auth_user.user_id,
457 payload.message,
458 parent_id,
459 version,
460 snapshot,
461 payload.changes,
462 )
463 .await?;
464
465 Ok(Json(serde_json::to_value(commit)?))
466}
467
468async fn list_commits(
494 State(state): State<ApiState>,
495 Path(workspace_id): Path<Uuid>,
496 Extension(auth_user): Extension<AuthUser>,
497 Query(pagination): Query<PaginationQuery>,
498) -> Result<Json<serde_json::Value>> {
499 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
501
502 let limit = pagination.limit.clamp(1, 100);
504
505 let commits = state.history.get_history(workspace_id, Some(limit)).await?;
507
508 Ok(Json(serde_json::json!({
510 "commits": commits,
511 "pagination": {
512 "limit": limit,
513 "offset": pagination.offset,
514 }
515 })))
516}
517
518async fn get_commit(
532 State(state): State<ApiState>,
533 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
534 Extension(auth_user): Extension<AuthUser>,
535) -> Result<Json<serde_json::Value>> {
536 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
538
539 let commit = state.history.get_commit(commit_id).await?;
541
542 if commit.workspace_id != workspace_id {
544 return Err(CollabError::InvalidInput(
545 "Commit does not belong to this workspace".to_string(),
546 ));
547 }
548
549 Ok(Json(serde_json::to_value(commit)?))
550}
551
552async fn restore_to_commit(
572 State(state): State<ApiState>,
573 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
574 Extension(auth_user): Extension<AuthUser>,
575) -> Result<Json<serde_json::Value>> {
576 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
578 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
579 return Err(CollabError::AuthorizationFailed(
580 "Only Admins and Editors can restore workspaces".to_string(),
581 ));
582 }
583
584 let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
586
587 Ok(Json(serde_json::json!({
588 "workspace_id": workspace_id,
589 "commit_id": commit_id,
590 "restored_state": restored_state
591 })))
592}
593
594async fn create_snapshot(
615 State(state): State<ApiState>,
616 Path(workspace_id): Path<Uuid>,
617 Extension(auth_user): Extension<AuthUser>,
618 Json(payload): Json<CreateSnapshotRequest>,
619) -> Result<Json<serde_json::Value>> {
620 validate_snapshot_name(&payload.name)?;
622
623 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
625 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
626 return Err(CollabError::AuthorizationFailed(
627 "Only Admins and Editors can create snapshots".to_string(),
628 ));
629 }
630
631 let snapshot = state
633 .history
634 .create_snapshot(
635 workspace_id,
636 payload.name,
637 payload.description,
638 payload.commit_id,
639 auth_user.user_id,
640 )
641 .await?;
642
643 Ok(Json(serde_json::to_value(snapshot)?))
644}
645
646async fn list_snapshots(
657 State(state): State<ApiState>,
658 Path(workspace_id): Path<Uuid>,
659 Extension(auth_user): Extension<AuthUser>,
660) -> Result<Json<serde_json::Value>> {
661 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
663
664 let snapshots = state.history.list_snapshots(workspace_id).await?;
666
667 Ok(Json(serde_json::to_value(snapshots)?))
668}
669
670async fn get_snapshot(
681 State(state): State<ApiState>,
682 Path((workspace_id, name)): Path<(Uuid, String)>,
683 Extension(auth_user): Extension<AuthUser>,
684) -> Result<Json<serde_json::Value>> {
685 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
687
688 let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
690
691 Ok(Json(serde_json::to_value(snapshot)?))
692}
693
694#[derive(Debug, Deserialize)]
697pub struct ForkWorkspaceRequest {
698 pub name: Option<String>,
699 pub fork_point_commit_id: Option<Uuid>,
700}
701
702async fn fork_workspace(
704 State(state): State<ApiState>,
705 Path(workspace_id): Path<Uuid>,
706 Extension(auth_user): Extension<AuthUser>,
707 Json(payload): Json<ForkWorkspaceRequest>,
708) -> Result<Json<serde_json::Value>> {
709 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
711
712 let forked = state
714 .workspace
715 .fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
716 .await?;
717
718 Ok(Json(serde_json::to_value(forked)?))
719}
720
721async fn list_forks(
723 State(state): State<ApiState>,
724 Path(workspace_id): Path<Uuid>,
725 Extension(auth_user): Extension<AuthUser>,
726) -> Result<Json<serde_json::Value>> {
727 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
729
730 let forks = state.workspace.list_forks(workspace_id).await?;
732
733 Ok(Json(serde_json::to_value(forks)?))
734}
735
736#[derive(Debug, Deserialize)]
737pub struct MergeWorkspacesRequest {
738 pub source_workspace_id: Uuid,
739}
740
741async fn merge_workspaces(
743 State(state): State<ApiState>,
744 Path(target_workspace_id): Path<Uuid>,
745 Extension(auth_user): Extension<AuthUser>,
746 Json(payload): Json<MergeWorkspacesRequest>,
747) -> Result<Json<serde_json::Value>> {
748 let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
750 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
751 return Err(CollabError::AuthorizationFailed(
752 "Only Admins and Editors can merge workspaces".to_string(),
753 ));
754 }
755
756 let (merged_state, conflicts) = state
758 .merge
759 .merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
760 .await?;
761
762 Ok(Json(serde_json::json!({
763 "merged_state": merged_state,
764 "conflicts": conflicts,
765 "has_conflicts": !conflicts.is_empty()
766 })))
767}
768
769async fn list_merges(
771 State(state): State<ApiState>,
772 Path(workspace_id): Path<Uuid>,
773 Extension(auth_user): Extension<AuthUser>,
774) -> Result<Json<serde_json::Value>> {
775 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
777
778 let merges = state.merge.list_merges(workspace_id).await?;
780
781 Ok(Json(serde_json::to_value(merges)?))
782}
783
784#[derive(Debug, Deserialize)]
787pub struct CreateBackupRequest {
788 pub storage_backend: Option<String>,
789 pub format: Option<String>,
790 pub commit_id: Option<Uuid>,
791}
792
793async fn create_backup(
795 State(state): State<ApiState>,
796 Path(workspace_id): Path<Uuid>,
797 Extension(auth_user): Extension<AuthUser>,
798 Json(payload): Json<CreateBackupRequest>,
799) -> Result<Json<serde_json::Value>> {
800 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
802 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
803 return Err(CollabError::AuthorizationFailed(
804 "Only Admins and Editors can create backups".to_string(),
805 ));
806 }
807
808 let storage_backend = match payload.storage_backend.as_deref() {
810 Some("s3") => StorageBackend::S3,
811 Some("azure") => StorageBackend::Azure,
812 Some("gcs") => StorageBackend::Gcs,
813 Some("custom") => StorageBackend::Custom,
814 _ => StorageBackend::Local,
815 };
816
817 let backup = state
819 .backup
820 .backup_workspace(
821 workspace_id,
822 auth_user.user_id,
823 storage_backend,
824 payload.format,
825 payload.commit_id,
826 )
827 .await?;
828
829 Ok(Json(serde_json::to_value(backup)?))
830}
831
832async fn list_backups(
834 State(state): State<ApiState>,
835 Path(workspace_id): Path<Uuid>,
836 Extension(auth_user): Extension<AuthUser>,
837 Query(pagination): Query<PaginationQuery>,
838) -> Result<Json<serde_json::Value>> {
839 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
841
842 let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
844
845 Ok(Json(serde_json::to_value(backups)?))
846}
847
848async fn delete_backup(
850 State(state): State<ApiState>,
851 Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
852 Extension(auth_user): Extension<AuthUser>,
853) -> Result<StatusCode> {
854 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
856 if !matches!(member.role, UserRole::Admin) {
857 return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
858 }
859
860 state.backup.delete_backup(backup_id).await?;
862
863 Ok(StatusCode::NO_CONTENT)
864}
865
866#[derive(Debug, Deserialize)]
867pub struct RestoreWorkspaceRequest {
868 pub backup_id: Uuid,
869 pub target_workspace_id: Option<Uuid>,
870}
871
872async fn restore_workspace(
874 State(state): State<ApiState>,
875 Path(workspace_id): Path<Uuid>,
876 Extension(auth_user): Extension<AuthUser>,
877 Json(payload): Json<RestoreWorkspaceRequest>,
878) -> Result<Json<serde_json::Value>> {
879 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
881 if !matches!(member.role, UserRole::Admin) {
882 return Err(CollabError::AuthorizationFailed(
883 "Only Admins can restore workspaces".to_string(),
884 ));
885 }
886
887 let restored_id = state
889 .backup
890 .restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
891 .await?;
892
893 Ok(Json(serde_json::json!({
894 "workspace_id": restored_id,
895 "restored_from_backup": payload.backup_id
896 })))
897}
898
899async fn get_workspace_state(
903 State(state): State<ApiState>,
904 Path(workspace_id): Path<Uuid>,
905 Extension(auth_user): Extension<AuthUser>,
906 Query(params): Query<std::collections::HashMap<String, String>>,
907) -> Result<Json<serde_json::Value>> {
908 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
910
911 let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
913
914 let sync_state = if let Some(version) = version {
916 state.sync.load_state_snapshot(workspace_id, Some(version)).await?
917 } else {
918 if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
920 let workspace = state.workspace.get_workspace(workspace_id).await?;
922 return Ok(Json(serde_json::json!({
923 "workspace_id": workspace_id,
924 "version": workspace.version,
925 "state": full_state,
926 "last_updated": workspace.updated_at
927 })));
928 }
929
930 state.sync.get_state(workspace_id)
932 };
933
934 if let Some(state_val) = sync_state {
935 Ok(Json(serde_json::json!({
936 "workspace_id": workspace_id,
937 "version": state_val.version,
938 "state": state_val.state,
939 "last_updated": state_val.last_updated
940 })))
941 } else {
942 let workspace = state.workspace.get_workspace(workspace_id).await?;
944 Ok(Json(serde_json::json!({
945 "workspace_id": workspace_id,
946 "version": workspace.version,
947 "state": workspace.config,
948 "last_updated": workspace.updated_at
949 })))
950 }
951}
952
953#[derive(Debug, Deserialize)]
954pub struct UpdateWorkspaceStateRequest {
955 pub state: serde_json::Value,
956}
957
958async fn update_workspace_state(
960 State(state): State<ApiState>,
961 Path(workspace_id): Path<Uuid>,
962 Extension(auth_user): Extension<AuthUser>,
963 Json(payload): Json<UpdateWorkspaceStateRequest>,
964) -> Result<Json<serde_json::Value>> {
965 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
967 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
968 return Err(CollabError::AuthorizationFailed(
969 "Only Admins and Editors can update workspace state".to_string(),
970 ));
971 }
972
973 state.sync.update_state(workspace_id, payload.state.clone())?;
975
976 let workspace = state.workspace.get_workspace(workspace_id).await?;
978 state
979 .sync
980 .record_state_change(
981 workspace_id,
982 "full_sync",
983 payload.state.clone(),
984 workspace.version + 1,
985 auth_user.user_id,
986 )
987 .await?;
988
989 Ok(Json(serde_json::json!({
990 "workspace_id": workspace_id,
991 "version": workspace.version + 1,
992 "state": payload.state
993 })))
994}
995
996async fn get_state_history(
998 State(state): State<ApiState>,
999 Path(workspace_id): Path<Uuid>,
1000 Extension(auth_user): Extension<AuthUser>,
1001 Query(params): Query<std::collections::HashMap<String, String>>,
1002) -> Result<Json<serde_json::Value>> {
1003 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
1005
1006 let since_version =
1008 params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
1009
1010 let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
1012
1013 Ok(Json(serde_json::json!({
1014 "workspace_id": workspace_id,
1015 "since_version": since_version,
1016 "changes": changes
1017 })))
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn test_router_creation() {
1026 use crate::events::EventBus;
1028 let event_bus = Arc::new(EventBus::new(100));
1029 let state = ApiState {
1030 auth: Arc::new(AuthService::new("test".to_string())),
1031 user: Arc::new(UserService::new(
1032 todo!(),
1033 Arc::new(AuthService::new("test".to_string())),
1034 )),
1035 workspace: Arc::new(WorkspaceService::new(todo!())),
1036 history: Arc::new(VersionControl::new(todo!())),
1037 merge: Arc::new(MergeService::new(todo!())),
1038 backup: Arc::new(BackupService::new(todo!(), None, todo!(), todo!())),
1039 sync: Arc::new(SyncEngine::new(event_bus)),
1040 };
1041 let _router = create_router(state);
1042 }
1043}