use crate::auth::{AuthService, Credentials};
use crate::backup::{BackupService, StorageBackend};
use crate::error::{CollabError, Result};
use crate::history::VersionControl;
use crate::merge::MergeService;
use crate::middleware::{auth_middleware, AuthUser};
use crate::models::UserRole;
use crate::sync::SyncEngine;
use crate::user::UserService;
use crate::workspace::WorkspaceService;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
middleware,
response::{IntoResponse, Response},
routing::{delete, get, post, put},
Extension, Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Clone)]
pub struct ApiState {
pub auth: Arc<AuthService>,
pub user: Arc<UserService>,
pub workspace: Arc<WorkspaceService>,
pub history: Arc<VersionControl>,
pub merge: Arc<MergeService>,
pub backup: Arc<BackupService>,
pub sync: Arc<SyncEngine>,
}
pub fn create_router(state: ApiState) -> Router {
let public_routes = Router::new()
.route("/auth/register", post(register))
.route("/auth/login", post(login))
.route("/health", get(health_check))
.route("/ready", get(readiness_check));
let protected_routes = Router::new()
.route("/workspaces", post(create_workspace))
.route("/workspaces", get(list_workspaces))
.route("/workspaces/{id}", get(get_workspace))
.route("/workspaces/{id}", put(update_workspace))
.route("/workspaces/{id}", delete(delete_workspace))
.route("/workspaces/{id}/members", post(add_member))
.route("/workspaces/{id}/members/{user_id}", delete(remove_member))
.route("/workspaces/{id}/members/{user_id}/role", put(change_role))
.route("/workspaces/{id}/members", get(list_members))
.route("/workspaces/{id}/commits", post(create_commit))
.route("/workspaces/{id}/commits", get(list_commits))
.route("/workspaces/{id}/commits/{commit_id}", get(get_commit))
.route("/workspaces/{id}/restore/{commit_id}", post(restore_to_commit))
.route("/workspaces/{id}/snapshots", post(create_snapshot))
.route("/workspaces/{id}/snapshots", get(list_snapshots))
.route("/workspaces/{id}/snapshots/{name}", get(get_snapshot))
.route("/workspaces/{id}/fork", post(fork_workspace))
.route("/workspaces/{id}/forks", get(list_forks))
.route("/workspaces/{id}/merge", post(merge_workspaces))
.route("/workspaces/{id}/merges", get(list_merges))
.route("/workspaces/{id}/backup", post(create_backup))
.route("/workspaces/{id}/backups", get(list_backups))
.route("/workspaces/{id}/backups/{backup_id}", delete(delete_backup))
.route("/workspaces/{id}/restore", post(restore_workspace))
.route("/workspaces/{id}/state", get(get_workspace_state))
.route("/workspaces/{id}/state", post(update_workspace_state))
.route("/workspaces/{id}/state/history", get(get_state_history))
.route_layer(middleware::from_fn_with_state(
state.auth.clone(),
auth_middleware,
));
Router::new().merge(public_routes).merge(protected_routes).with_state(state)
}
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct AuthResponse {
pub access_token: String,
pub token_type: String,
pub expires_at: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateWorkspaceRequest {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateWorkspaceRequest {
pub name: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AddMemberRequest {
pub user_id: Uuid,
pub role: UserRole,
}
#[derive(Debug, Deserialize)]
pub struct ChangeRoleRequest {
pub role: UserRole,
}
#[derive(Debug, Deserialize)]
pub struct CreateCommitRequest {
pub message: String,
pub changes: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub struct CreateSnapshotRequest {
pub name: String,
pub description: Option<String>,
pub commit_id: Uuid,
}
#[derive(Debug, Deserialize)]
pub struct PaginationQuery {
#[serde(default = "default_limit")]
pub limit: i32,
#[serde(default)]
pub offset: i32,
}
const fn default_limit() -> i32 {
50
}
impl IntoResponse for CollabError {
fn into_response(self) -> Response {
let (status, message) = match &self {
Self::AuthenticationFailed(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
Self::AuthorizationFailed(msg) => (StatusCode::FORBIDDEN, msg.clone()),
Self::WorkspaceNotFound(msg) | Self::UserNotFound(msg) => {
(StatusCode::NOT_FOUND, msg.clone())
}
Self::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
Self::AlreadyExists(msg) | Self::ConflictDetected(msg) => {
(StatusCode::CONFLICT, msg.clone())
}
Self::Timeout(msg) => (StatusCode::REQUEST_TIMEOUT, msg.clone()),
Self::Internal(msg)
| Self::DatabaseError(msg)
| Self::SerializationError(msg)
| Self::SyncError(msg)
| Self::WebSocketError(msg)
| Self::ConnectionError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
Self::VersionMismatch { expected, actual } => (
StatusCode::CONFLICT,
format!("Version mismatch: expected {expected}, got {actual}"),
),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}
async fn register(
State(state): State<ApiState>,
Json(payload): Json<RegisterRequest>,
) -> Result<Json<AuthResponse>> {
let user = state
.user
.create_user(payload.username, payload.email, payload.password)
.await?;
let token = state.auth.generate_token(&user)?;
Ok(Json(AuthResponse {
access_token: token.access_token,
token_type: token.token_type,
expires_at: token.expires_at.to_rfc3339(),
}))
}
async fn login(
State(state): State<ApiState>,
Json(payload): Json<Credentials>,
) -> Result<Json<AuthResponse>> {
let user = state.user.authenticate(&payload.username, &payload.password).await?;
let token = state.auth.generate_token(&user)?;
Ok(Json(AuthResponse {
access_token: token.access_token,
token_type: token.token_type,
expires_at: token.expires_at.to_rfc3339(),
}))
}
async fn create_workspace(
State(state): State<ApiState>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<CreateWorkspaceRequest>,
) -> Result<Json<serde_json::Value>> {
let workspace = state
.workspace
.create_workspace(payload.name, payload.description, auth_user.user_id)
.await?;
Ok(Json(serde_json::to_value(workspace)?))
}
async fn list_workspaces(
State(state): State<ApiState>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let workspaces = state.workspace.list_user_workspaces(auth_user.user_id).await?;
Ok(Json(serde_json::to_value(workspaces)?))
}
async fn get_workspace(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(id, auth_user.user_id).await?;
let workspace = state.workspace.get_workspace(id).await?;
Ok(Json(serde_json::to_value(workspace)?))
}
async fn update_workspace(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<UpdateWorkspaceRequest>,
) -> Result<Json<serde_json::Value>> {
let workspace = state
.workspace
.update_workspace(id, auth_user.user_id, payload.name, payload.description, None)
.await?;
Ok(Json(serde_json::to_value(workspace)?))
}
async fn delete_workspace(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<StatusCode> {
state.workspace.delete_workspace(id, auth_user.user_id).await?;
Ok(StatusCode::NO_CONTENT)
}
async fn add_member(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<AddMemberRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state
.workspace
.add_member(workspace_id, auth_user.user_id, payload.user_id, payload.role)
.await?;
Ok(Json(serde_json::to_value(member)?))
}
async fn remove_member(
State(state): State<ApiState>,
Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<StatusCode> {
state
.workspace
.remove_member(workspace_id, auth_user.user_id, member_user_id)
.await?;
Ok(StatusCode::NO_CONTENT)
}
async fn change_role(
State(state): State<ApiState>,
Path((workspace_id, member_user_id)): Path<(Uuid, Uuid)>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<ChangeRoleRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state
.workspace
.change_role(workspace_id, auth_user.user_id, member_user_id, payload.role)
.await?;
Ok(Json(serde_json::to_value(member)?))
}
async fn list_members(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let members = state.workspace.list_members(workspace_id).await?;
Ok(Json(serde_json::to_value(members)?))
}
async fn health_check() -> impl IntoResponse {
Json(serde_json::json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
async fn readiness_check(State(state): State<ApiState>) -> impl IntoResponse {
let db_healthy = state.workspace.check_database_health().await;
if db_healthy {
Json(serde_json::json!({
"status": "ready",
"database": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
.into_response()
} else {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"status": "not_ready",
"database": "unhealthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
})),
)
.into_response()
}
}
fn validate_commit_message(message: &str) -> Result<()> {
if message.is_empty() {
return Err(CollabError::InvalidInput("Commit message cannot be empty".to_string()));
}
if message.len() > 500 {
return Err(CollabError::InvalidInput(
"Commit message cannot exceed 500 characters".to_string(),
));
}
Ok(())
}
fn validate_snapshot_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(CollabError::InvalidInput("Snapshot name cannot be empty".to_string()));
}
if name.len() > 100 {
return Err(CollabError::InvalidInput(
"Snapshot name cannot exceed 100 characters".to_string(),
));
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
return Err(CollabError::InvalidInput(
"Snapshot name can only contain alphanumeric characters, hyphens, underscores, and dots".to_string(),
));
}
Ok(())
}
async fn create_commit(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<CreateCommitRequest>,
) -> Result<Json<serde_json::Value>> {
validate_commit_message(&payload.message)?;
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can create commits".to_string(),
));
}
let workspace = state.workspace.get_workspace(workspace_id).await?;
let parent = state.history.get_latest_commit(workspace_id).await?;
let parent_id = parent.as_ref().map(|c| c.id);
let version = parent.as_ref().map_or(1, |c| c.version + 1);
let snapshot = serde_json::to_value(&workspace)?;
let commit = state
.history
.create_commit(
workspace_id,
auth_user.user_id,
payload.message,
parent_id,
version,
snapshot,
payload.changes,
)
.await?;
Ok(Json(serde_json::to_value(commit)?))
}
async fn list_commits(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Query(pagination): Query<PaginationQuery>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let limit = pagination.limit.clamp(1, 100);
let commits = state.history.get_history(workspace_id, Some(limit)).await?;
Ok(Json(serde_json::json!({
"commits": commits,
"pagination": {
"limit": limit,
"offset": pagination.offset,
}
})))
}
async fn get_commit(
State(state): State<ApiState>,
Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let commit = state.history.get_commit(commit_id).await?;
if commit.workspace_id != workspace_id {
return Err(CollabError::InvalidInput(
"Commit does not belong to this workspace".to_string(),
));
}
Ok(Json(serde_json::to_value(commit)?))
}
async fn restore_to_commit(
State(state): State<ApiState>,
Path((workspace_id, commit_id)): Path<(Uuid, Uuid)>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can restore workspaces".to_string(),
));
}
let restored_state = state.history.restore_to_commit(workspace_id, commit_id).await?;
Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"commit_id": commit_id,
"restored_state": restored_state
})))
}
async fn create_snapshot(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<CreateSnapshotRequest>,
) -> Result<Json<serde_json::Value>> {
validate_snapshot_name(&payload.name)?;
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can create snapshots".to_string(),
));
}
let snapshot = state
.history
.create_snapshot(
workspace_id,
payload.name,
payload.description,
payload.commit_id,
auth_user.user_id,
)
.await?;
Ok(Json(serde_json::to_value(snapshot)?))
}
async fn list_snapshots(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let snapshots = state.history.list_snapshots(workspace_id).await?;
Ok(Json(serde_json::to_value(snapshots)?))
}
async fn get_snapshot(
State(state): State<ApiState>,
Path((workspace_id, name)): Path<(Uuid, String)>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let snapshot = state.history.get_snapshot(workspace_id, &name).await?;
Ok(Json(serde_json::to_value(snapshot)?))
}
#[derive(Debug, Deserialize)]
pub struct ForkWorkspaceRequest {
#[serde(alias = "new_name")]
pub name: Option<String>,
pub fork_point_commit_id: Option<Uuid>,
}
async fn fork_workspace(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<ForkWorkspaceRequest>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let forked = state
.workspace
.fork_workspace(workspace_id, payload.name, auth_user.user_id, payload.fork_point_commit_id)
.await?;
Ok(Json(serde_json::to_value(forked)?))
}
async fn list_forks(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let forks = state.workspace.list_forks(workspace_id).await?;
Ok(Json(serde_json::to_value(forks)?))
}
#[derive(Debug, Deserialize)]
pub struct MergeWorkspacesRequest {
pub source_workspace_id: Uuid,
}
async fn merge_workspaces(
State(state): State<ApiState>,
Path(target_workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<MergeWorkspacesRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state.workspace.get_member(target_workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can merge workspaces".to_string(),
));
}
let (merged_state, conflicts) = state
.merge
.merge_workspaces(payload.source_workspace_id, target_workspace_id, auth_user.user_id)
.await?;
Ok(Json(serde_json::json!({
"workspace": merged_state,
"conflicts": conflicts,
"has_conflicts": !conflicts.is_empty()
})))
}
async fn list_merges(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let merges = state.merge.list_merges(workspace_id).await?;
Ok(Json(serde_json::to_value(merges)?))
}
#[derive(Debug, Deserialize)]
pub struct CreateBackupRequest {
pub storage_backend: Option<String>,
pub format: Option<String>,
pub commit_id: Option<Uuid>,
}
#[allow(clippy::large_futures)]
async fn create_backup(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<CreateBackupRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can create backups".to_string(),
));
}
let storage_backend = match payload.storage_backend.as_deref() {
Some("s3") => StorageBackend::S3,
Some("azure") => StorageBackend::Azure,
Some("gcs") => StorageBackend::Gcs,
Some("custom") => StorageBackend::Custom,
_ => StorageBackend::Local,
};
let backup = state
.backup
.backup_workspace(
workspace_id,
auth_user.user_id,
storage_backend,
payload.format,
payload.commit_id,
)
.await?;
Ok(Json(serde_json::to_value(backup)?))
}
async fn list_backups(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Query(pagination): Query<PaginationQuery>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let backups = state.backup.list_backups(workspace_id, Some(pagination.limit)).await?;
Ok(Json(serde_json::to_value(backups)?))
}
async fn delete_backup(
State(state): State<ApiState>,
Path((workspace_id, backup_id)): Path<(Uuid, Uuid)>,
Extension(auth_user): Extension<AuthUser>,
) -> Result<StatusCode> {
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin) {
return Err(CollabError::AuthorizationFailed("Only Admins can delete backups".to_string()));
}
state.backup.delete_backup(backup_id).await?;
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Deserialize)]
pub struct RestoreWorkspaceRequest {
pub backup_id: Uuid,
pub target_workspace_id: Option<Uuid>,
}
async fn restore_workspace(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<RestoreWorkspaceRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin) {
return Err(CollabError::AuthorizationFailed(
"Only Admins can restore workspaces".to_string(),
));
}
let restored_id = state
.backup
.restore_workspace(payload.backup_id, payload.target_workspace_id, auth_user.user_id)
.await?;
Ok(Json(serde_json::json!({
"workspace_id": restored_id,
"restored_from_backup": payload.backup_id
})))
}
async fn get_workspace_state(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let version = params.get("version").and_then(|v| v.parse::<i64>().ok());
let sync_state = if let Some(version) = version {
state.sync.load_state_snapshot(workspace_id, Some(version)).await?
} else {
if let Ok(Some(full_state)) = state.sync.get_full_workspace_state(workspace_id).await {
let workspace = state.workspace.get_workspace(workspace_id).await?;
return Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"version": workspace.version,
"state": full_state,
"last_updated": workspace.updated_at
})));
}
state.sync.get_state(workspace_id)
};
if let Some(state_val) = sync_state {
Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"version": state_val.version,
"state": state_val.state,
"last_updated": state_val.last_updated
})))
} else {
let workspace = state.workspace.get_workspace(workspace_id).await?;
Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"version": workspace.version,
"state": workspace.config,
"last_updated": workspace.updated_at
})))
}
}
#[derive(Debug, Deserialize)]
pub struct UpdateWorkspaceStateRequest {
pub state: serde_json::Value,
}
async fn update_workspace_state(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Json(payload): Json<UpdateWorkspaceStateRequest>,
) -> Result<Json<serde_json::Value>> {
let member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
if !matches!(member.role, UserRole::Admin | UserRole::Editor) {
return Err(CollabError::AuthorizationFailed(
"Only Admins and Editors can update workspace state".to_string(),
));
}
state.sync.update_state(workspace_id, payload.state.clone())?;
let workspace = state.workspace.get_workspace(workspace_id).await?;
state
.sync
.record_state_change(
workspace_id,
"full_sync",
payload.state.clone(),
workspace.version + 1,
auth_user.user_id,
)
.await?;
Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"version": workspace.version + 1,
"state": payload.state
})))
}
async fn get_state_history(
State(state): State<ApiState>,
Path(workspace_id): Path<Uuid>,
Extension(auth_user): Extension<AuthUser>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<serde_json::Value>> {
let _member = state.workspace.get_member(workspace_id, auth_user.user_id).await?;
let since_version =
params.get("since_version").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
let changes = state.sync.get_state_changes_since(workspace_id, since_version).await?;
Ok(Json(serde_json::json!({
"workspace_id": workspace_id,
"since_version": since_version,
"changes": changes
})))
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_router_creation() {
use super::*;
use crate::core_bridge::CoreBridge;
use crate::events::EventBus;
use sqlx::SqlitePool;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let workspace_dir = temp_dir.path().join("workspaces");
let backup_dir = temp_dir.path().join("backups");
std::fs::create_dir_all(&workspace_dir).expect("Failed to create workspace dir");
std::fs::create_dir_all(&backup_dir).expect("Failed to create backup dir");
let db = SqlitePool::connect("sqlite::memory:")
.await
.expect("Failed to create database pool");
sqlx::migrate!("./migrations").run(&db).await.expect("Failed to run migrations");
let core_bridge = Arc::new(CoreBridge::new(&workspace_dir));
let auth = Arc::new(AuthService::new("test-secret-key".to_string()));
let user = Arc::new(UserService::new(db.clone(), auth.clone()));
let workspace =
Arc::new(WorkspaceService::with_core_bridge(db.clone(), core_bridge.clone()));
let history = Arc::new(VersionControl::new(db.clone()));
let merge = Arc::new(MergeService::new(db.clone()));
let backup = Arc::new(BackupService::new(
db.clone(),
Some(backup_dir.to_string_lossy().to_string()),
core_bridge,
workspace.clone(),
));
let event_bus = Arc::new(EventBus::new(100));
let sync = Arc::new(SyncEngine::new(event_bus));
let state = ApiState {
auth,
user,
workspace,
history,
merge,
backup,
sync,
};
let _router = create_router(state);
}
}