1use crate::auth::{AuthService, Credentials};
4use crate::error::{CollabError, Result};
5use crate::history::VersionControl;
6use crate::middleware::{auth_middleware, AuthUser};
7use crate::models::UserRole;
8use crate::user::UserService;
9use crate::workspace::WorkspaceService;
10use axum::{
11 extract::{Path, Query, State},
12 http::StatusCode,
13 middleware,
14 response::{IntoResponse, Response},
15 routing::{delete, get, post, put},
16 Extension, Json, Router,
17};
18use serde::{Deserialize, Serialize};
19use std::sync::Arc;
20use uuid::Uuid;
21
22#[derive(Clone)]
24pub struct ApiState {
25 pub auth: Arc<AuthService>,
26 pub user: Arc<UserService>,
27 pub workspace: Arc<WorkspaceService>,
28 pub history: Arc<VersionControl>,
29}
30
31pub fn create_router(state: ApiState) -> Router {
33 let public_routes = Router::new()
35 .route("/auth/register", post(register))
36 .route("/auth/login", post(login))
37 .route("/health", get(health_check))
38 .route("/ready", get(readiness_check));
39
40 let protected_routes = Router::new()
42 .route("/workspaces", post(create_workspace))
44 .route("/workspaces", get(list_workspaces))
45 .route("/workspaces/:id", get(get_workspace))
46 .route("/workspaces/:id", put(update_workspace))
47 .route("/workspaces/:id", delete(delete_workspace))
48 .route("/workspaces/:id/members", post(add_member))
50 .route("/workspaces/:id/members/:user_id", delete(remove_member))
51 .route("/workspaces/:id/members/:user_id/role", put(change_role))
52 .route("/workspaces/:id/members", get(list_members))
53 .route("/workspaces/:id/commits", post(create_commit))
55 .route("/workspaces/:id/commits", get(list_commits))
56 .route("/workspaces/:id/commits/:commit_id", get(get_commit))
57 .route("/workspaces/:id/restore/:commit_id", post(restore_to_commit))
58 .route("/workspaces/:id/snapshots", post(create_snapshot))
60 .route("/workspaces/:id/snapshots", get(list_snapshots))
61 .route("/workspaces/:id/snapshots/:name", get(get_snapshot))
62 .route_layer(middleware::from_fn_with_state(
63 state.auth.clone(),
64 auth_middleware,
65 ));
66
67 Router::new().merge(public_routes).merge(protected_routes).with_state(state)
69}
70
71#[derive(Debug, Deserialize)]
74pub struct RegisterRequest {
75 pub username: String,
76 pub email: String,
77 pub password: String,
78}
79
80#[derive(Debug, Serialize)]
81pub struct AuthResponse {
82 pub access_token: String,
83 pub token_type: String,
84 pub expires_at: String,
85}
86
87#[derive(Debug, Deserialize)]
88pub struct CreateWorkspaceRequest {
89 pub name: String,
90 pub description: Option<String>,
91}
92
93#[derive(Debug, Deserialize)]
94pub struct UpdateWorkspaceRequest {
95 pub name: Option<String>,
96 pub description: Option<String>,
97}
98
99#[derive(Debug, Deserialize)]
100pub struct AddMemberRequest {
101 pub user_id: Uuid,
102 pub role: UserRole,
103}
104
105#[derive(Debug, Deserialize)]
106pub struct ChangeRoleRequest {
107 pub role: UserRole,
108}
109
110#[derive(Debug, Deserialize)]
111pub struct CreateCommitRequest {
112 pub message: String,
113 pub changes: serde_json::Value,
114}
115
116#[derive(Debug, Deserialize)]
117pub struct CreateSnapshotRequest {
118 pub name: String,
119 pub description: Option<String>,
120 pub commit_id: Uuid,
121}
122
123#[derive(Debug, Deserialize)]
124pub struct PaginationQuery {
125 #[serde(default = "default_limit")]
126 pub limit: i32,
127 #[serde(default)]
128 pub offset: i32,
129}
130
131fn default_limit() -> i32 {
132 50
133}
134
135impl IntoResponse for CollabError {
138 fn into_response(self) -> Response {
139 let (status, message) = match self {
140 CollabError::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg),
141 CollabError::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg),
142 CollabError::WorkspaceNotFound(msg) => (StatusCode::NOT_FOUND, msg),
143 CollabError::UserNotFound(msg) => (StatusCode::NOT_FOUND, msg),
144 CollabError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
145 CollabError::AlreadyExists(msg) => (StatusCode::CONFLICT, msg),
146 CollabError::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg),
147 _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()),
148 };
149
150 (status, Json(serde_json::json!({ "error": message }))).into_response()
151 }
152}
153
154async fn register(
158 State(state): State<ApiState>,
159 Json(payload): Json<RegisterRequest>,
160) -> Result<Json<AuthResponse>> {
161 let user = state
163 .user
164 .create_user(payload.username, payload.email, payload.password)
165 .await?;
166
167 let token = state.auth.generate_token(&user)?;
169
170 Ok(Json(AuthResponse {
171 access_token: token.access_token,
172 token_type: token.token_type,
173 expires_at: token.expires_at.to_rfc3339(),
174 }))
175}
176
177async fn login(
179 State(state): State<ApiState>,
180 Json(payload): Json<Credentials>,
181) -> Result<Json<AuthResponse>> {
182 let user = state.user.authenticate(&payload.username, &payload.password).await?;
184
185 let token = state.auth.generate_token(&user)?;
187
188 Ok(Json(AuthResponse {
189 access_token: token.access_token,
190 token_type: token.token_type,
191 expires_at: token.expires_at.to_rfc3339(),
192 }))
193}
194
195async fn create_workspace(
197 State(state): State<ApiState>,
198 Extension(auth_user): Extension<AuthUser>,
199 Json(payload): Json<CreateWorkspaceRequest>,
200) -> Result<Json<serde_json::Value>> {
201 let workspace = state
203 .workspace
204 .create_workspace(payload.name, payload.description, auth_user.user_id)
205 .await?;
206
207 Ok(Json(serde_json::to_value(workspace)?))
208}
209
210async fn list_workspaces(
212 State(state): State<ApiState>,
213 Extension(auth_user): Extension<AuthUser>,
214) -> Result<Json<serde_json::Value>> {
215 let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
217
218 Ok(Json(serde_json::to_value(workspaces)?))
219}
220
221async fn get_workspace(
223 State(state): State<ApiState>,
224 Path(id): Path<Uuid>,
225 Extension(auth_user): Extension<AuthUser>,
226) -> Result<Json<serde_json::Value>> {
227 let _member = state.workspace.get_member(id, auth_user.user_id).await?;
229
230 let workspace = state.workspace.get_workspace(id).await?;
232
233 Ok(Json(serde_json::to_value(workspace)?))
234}
235
236async fn update_workspace(
238 State(state): State<ApiState>,
239 Path(id): Path<Uuid>,
240 Extension(auth_user): Extension<AuthUser>,
241 Json(payload): Json<UpdateWorkspaceRequest>,
242) -> Result<Json<serde_json::Value>> {
243 let workspace = state
245 .workspace
246 .update_workspace(id, auth_user.user_id, payload.name, payload.description, None)
247 .await?;
248
249 Ok(Json(serde_json::to_value(workspace)?))
250}
251
252async fn delete_workspace(
254 State(state): State<ApiState>,
255 Path(id): Path<Uuid>,
256 Extension(auth_user): Extension<AuthUser>,
257) -> Result<StatusCode> {
258 state.workspace.delete_workspace(id, auth_user.user_id).await?;
260
261 Ok(StatusCode::NO_CONTENT)
262}
263
264async fn add_member(
266 State(state): State<ApiState>,
267 Path(workspace_id): Path<Uuid>,
268 Extension(auth_user): Extension<AuthUser>,
269 Json(payload): Json<AddMemberRequest>,
270) -> Result<Json<serde_json::Value>> {
271 let member = state
273 .workspace
274 .add_member(workspace_id, auth_user.user_id, payload.user_id, payload.role)
275 .await?;
276
277 Ok(Json(serde_json::to_value(member)?))
278}
279
280async fn remove_member(
282 State(state): State<ApiState>,
283 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
284 Extension(auth_user): Extension<AuthUser>,
285) -> Result<StatusCode> {
286 state
288 .workspace
289 .remove_member(workspace_id, auth_user.user_id, member_user_id)
290 .await?;
291
292 Ok(StatusCode::NO_CONTENT)
293}
294
295async fn change_role(
297 State(state): State<ApiState>,
298 Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
299 Extension(auth_user): Extension<AuthUser>,
300 Json(payload): Json<ChangeRoleRequest>,
301) -> Result<Json<serde_json::Value>> {
302 let member = state
304 .workspace
305 .change_role(workspace_id, auth_user.user_id, member_user_id, payload.role)
306 .await?;
307
308 Ok(Json(serde_json::to_value(member)?))
309}
310
311async fn list_members(
313 State(state): State<ApiState>,
314 Path(workspace_id): Path<Uuid>,
315 Extension(auth_user): Extension<AuthUser>,
316) -> Result<Json<serde_json::Value>> {
317 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
319
320 let members = state.workspace.list_members(workspace_id).await?;
322
323 Ok(Json(serde_json::to_value(members)?))
324}
325
326async fn health_check() -> impl IntoResponse {
328 StatusCode::OK
329}
330
331async fn readiness_check() -> impl IntoResponse {
333 StatusCode::OK
335}
336
337fn validate_commit_message(message: &str) -> Result<()> {
341 if message.is_empty() {
342 return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
343 }
344 if message.len() > 500 {
345 return Err(CollabError::InvalidInput(
346 "Commit message cannot exceed 500 characters".to_string(),
347 ));
348 }
349 Ok(())
350}
351
352fn validate_snapshot_name(name: &str) -> Result<()> {
354 if name.is_empty() {
355 return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
356 }
357 if name.len() > 100 {
358 return Err(CollabError::InvalidInput(
359 "Snapshot name cannot exceed 100 characters".to_string(),
360 ));
361 }
362 if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
364 return Err(CollabError::InvalidInput(
365 "Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
366 ));
367 }
368 Ok(())
369}
370
371async fn create_commit(
404 State(state): State<ApiState>,
405 Path(workspace_id): Path<Uuid>,
406 Extension(auth_user): Extension<AuthUser>,
407 Json(payload): Json<CreateCommitRequest>,
408) -> Result<Json<serde_json::Value>> {
409 validate_commit_message(&payload.message)?;
411
412 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
414 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
415 return Err(CollabError::AuthorizationFailed(
416 "Only Admins and Editors can create commits".to_string(),
417 ));
418 }
419
420 let workspace = state.workspace.get_workspace(workspace_id).await?;
422
423 let parent = state.history.get_latest_commit(workspace_id).await?;
425 let parent_id = parent.as_ref().map(|c| c.id);
426 let version = parent.as_ref().map(|c| c.version + 1).unwrap_or(1);
427
428 let snapshot = serde_json::to_value(&workspace)?;
430
431 let commit = state
433 .history
434 .create_commit(
435 workspace_id,
436 auth_user.user_id,
437 payload.message,
438 parent_id,
439 version,
440 snapshot,
441 payload.changes,
442 )
443 .await?;
444
445 Ok(Json(serde_json::to_value(commit)?))
446}
447
448async fn list_commits(
474 State(state): State<ApiState>,
475 Path(workspace_id): Path<Uuid>,
476 Extension(auth_user): Extension<AuthUser>,
477 Query(pagination): Query<PaginationQuery>,
478) -> Result<Json<serde_json::Value>> {
479 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
481
482 let limit = pagination.limit.clamp(1, 100);
484
485 let commits = state.history.get_history(workspace_id, Some(limit)).await?;
487
488 Ok(Json(serde_json::json!({
490 "commits": commits,
491 "pagination": {
492 "limit": limit,
493 "offset": pagination.offset,
494 }
495 })))
496}
497
498async fn get_commit(
512 State(state): State<ApiState>,
513 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
514 Extension(auth_user): Extension<AuthUser>,
515) -> Result<Json<serde_json::Value>> {
516 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
518
519 let commit = state.history.get_commit(commit_id).await?;
521
522 if commit.workspace_id != workspace_id {
524 return Err(CollabError::InvalidInput(
525 "Commit does not belong to this workspace".to_string(),
526 ));
527 }
528
529 Ok(Json(serde_json::to_value(commit)?))
530}
531
532async fn restore_to_commit(
552 State(state): State<ApiState>,
553 Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
554 Extension(auth_user): Extension<AuthUser>,
555) -> Result<Json<serde_json::Value>> {
556 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
558 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
559 return Err(CollabError::AuthorizationFailed(
560 "Only Admins and Editors can restore workspaces".to_string(),
561 ));
562 }
563
564 let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
566
567 Ok(Json(serde_json::json!({
568 "workspace_id": workspace_id,
569 "commit_id": commit_id,
570 "restored_state": restored_state
571 })))
572}
573
574async fn create_snapshot(
595 State(state): State<ApiState>,
596 Path(workspace_id): Path<Uuid>,
597 Extension(auth_user): Extension<AuthUser>,
598 Json(payload): Json<CreateSnapshotRequest>,
599) -> Result<Json<serde_json::Value>> {
600 validate_snapshot_name(&payload.name)?;
602
603 let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
605 if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
606 return Err(CollabError::AuthorizationFailed(
607 "Only Admins and Editors can create snapshots".to_string(),
608 ));
609 }
610
611 let snapshot = state
613 .history
614 .create_snapshot(
615 workspace_id,
616 payload.name,
617 payload.description,
618 payload.commit_id,
619 auth_user.user_id,
620 )
621 .await?;
622
623 Ok(Json(serde_json::to_value(snapshot)?))
624}
625
626async fn list_snapshots(
637 State(state): State<ApiState>,
638 Path(workspace_id): Path<Uuid>,
639 Extension(auth_user): Extension<AuthUser>,
640) -> Result<Json<serde_json::Value>> {
641 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
643
644 let snapshots = state.history.list_snapshots(workspace_id).await?;
646
647 Ok(Json(serde_json::to_value(snapshots)?))
648}
649
650async fn get_snapshot(
661 State(state): State<ApiState>,
662 Path((workspace_id, name)): Path<(Uuid, String)>,
663 Extension(auth_user): Extension<AuthUser>,
664) -> Result<Json<serde_json::Value>> {
665 let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
667
668 let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
670
671 Ok(Json(serde_json::to_value(snapshot)?))
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 #[test]
679 fn test_router_creation() {
680 let state = ApiState {
682 auth: Arc::new(AuthService::new("test".to_string())),
683 user: Arc::new(UserService::new(
684 todo!(),
685 Arc::new(AuthService::new("test".to_string())),
686 )),
687 workspace: Arc::new(WorkspaceService::new(todo!())),
688 history: Arc::new(VersionControl::new(todo!())),
689 };
690 let _router = create_router(state);
691 }
692}