jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): add Axum server skeleton with basic routing

+161
+161
crates/tandem-server/src/main.rs
··· 1 + //! Tandem Server 2 + //! 3 + //! Forge server built with Axum, Yrs, and SQLite 4 + 5 + mod auth; 6 + mod authz; 7 + mod db; 8 + mod docs; 9 + mod events; 10 + mod handlers; 11 + mod sync; 12 + 13 + use axum::{ 14 + Json, Router, 15 + middleware, 16 + routing::{get, post}, 17 + }; 18 + use std::sync::Arc; 19 + use tower_http::cors::CorsLayer; 20 + use tower_http::trace::TraceLayer; 21 + 22 + use auth::{auth_middleware, get_me, login}; 23 + use db::Database; 24 + use docs::DocManager; 25 + use events::EventManager; 26 + use sync::SyncManager; 27 + 28 + #[derive(Clone)] 29 + pub struct AppState { 30 + db: Database, 31 + docs: Arc<DocManager>, 32 + events: Arc<EventManager>, 33 + sync: Arc<SyncManager>, 34 + } 35 + 36 + #[tokio::main] 37 + async fn main() { 38 + tracing_subscriber::fmt() 39 + .with_env_filter( 40 + tracing_subscriber::EnvFilter::try_from_default_env() 41 + .unwrap_or_else(|_| "tandem_server=debug,tower_http=debug".into()), 42 + ) 43 + .init(); 44 + 45 + let database_url = 46 + std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:tandem.db".to_string()); 47 + 48 + let db = Database::new(&database_url) 49 + .await 50 + .expect("Failed to connect to database"); 51 + 52 + db.run_migrations().await.expect("Failed to run migrations"); 53 + 54 + tracing::info!("Database initialized and migrations applied"); 55 + 56 + let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| "data".to_string()); 57 + let docs = Arc::new(DocManager::new(&data_dir)); 58 + tracing::info!("DocManager initialized with data directory: {}", data_dir); 59 + 60 + let events = Arc::new(EventManager::new()); 61 + tracing::info!("EventManager initialized"); 62 + 63 + let sync = Arc::new(SyncManager::new()); 64 + tracing::info!("SyncManager initialized"); 65 + 66 + let state = AppState { 67 + db, 68 + docs: Arc::clone(&docs), 69 + events, 70 + sync, 71 + }; 72 + 73 + let app = Router::new() 74 + .nest("/api", api_routes(state.clone())) 75 + .nest("/sync", sync_routes()) 76 + .nest("/events", event_routes()) 77 + .route("/health", get(health_check)) 78 + .layer(CorsLayer::permissive()) 79 + .layer(TraceLayer::new_for_http()) 80 + .with_state(state); 81 + 82 + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 83 + println!("Server running on http://localhost:3000"); 84 + 85 + axum::serve(listener, app) 86 + .with_graceful_shutdown(shutdown_signal(docs)) 87 + .await 88 + .unwrap(); 89 + } 90 + 91 + async fn shutdown_signal(docs: Arc<DocManager>) { 92 + let ctrl_c = async { 93 + tokio::signal::ctrl_c() 94 + .await 95 + .expect("Failed to install Ctrl+C handler"); 96 + }; 97 + 98 + #[cfg(unix)] 99 + let terminate = async { 100 + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) 101 + .expect("Failed to install SIGTERM handler") 102 + .recv() 103 + .await; 104 + }; 105 + 106 + #[cfg(not(unix))] 107 + let terminate = std::future::pending::<()>(); 108 + 109 + tokio::select! { 110 + _ = ctrl_c => { 111 + tracing::info!("Received Ctrl+C, shutting down gracefully"); 112 + }, 113 + _ = terminate => { 114 + tracing::info!("Received SIGTERM, shutting down gracefully"); 115 + }, 116 + } 117 + 118 + tracing::info!("Saving all Y.Doc states to disk"); 119 + if let Err(e) = docs.save_all().await { 120 + tracing::error!("Failed to save docs: {}", e); 121 + } else { 122 + tracing::info!("All Y.Doc states saved successfully"); 123 + } 124 + } 125 + 126 + fn api_routes(state: AppState) -> Router<AppState> { 127 + // Public routes (no authentication required) 128 + let public_routes = Router::new().route("/auth/login", post(login)); 129 + 130 + // Protected routes (authentication required) 131 + let protected_routes = Router::new() 132 + .route("/auth/me", get(get_me)) 133 + .route("/repos", get(handlers::repos::list_repos).post(handlers::repos::create_repo)) 134 + .route("/repos/:id", get(handlers::repos::get_repo)) 135 + .route("/repos/:id/changes", get(handlers::changes::list_changes)) 136 + .route("/repos/:id/changes/:cid", get(handlers::changes::get_change)) 137 + .route( 138 + "/repos/:id/bookmarks", 139 + get(handlers::bookmarks::list_bookmarks).post(handlers::bookmarks::move_bookmark), 140 + ) 141 + .route("/repos/:id/presence", get(handlers::presence::get_presence)) 142 + .route("/repos/:id/content/:hash", get(handlers::content::get_content)) 143 + .route_layer(middleware::from_fn_with_state( 144 + state.clone(), 145 + auth_middleware, 146 + )); 147 + 148 + public_routes.merge(protected_routes) 149 + } 150 + 151 + fn sync_routes() -> Router<AppState> { 152 + Router::new().route("/:repo_id", get(sync::sync_handler)) 153 + } 154 + 155 + fn event_routes() -> Router<AppState> { 156 + Router::new().route("/:repo_id", get(events::events_handler)) 157 + } 158 + 159 + async fn health_check() -> Json<serde_json::Value> { 160 + Json(serde_json::json!({"status": "ok"})) 161 + }