1use axum::{
2 Router,
3 extract::DefaultBodyLimit,
4 http::{Method, header::CONTENT_TYPE},
5 middleware,
6 routing::{delete, get, patch, post, put},
7};
8use std::sync::Arc;
9use tokio::net::TcpListener;
10use tower_http::cors::{Any, CorsLayer};
11use tracing::{info, warn};
12
13use crate::auth::{AuthState, auth_middleware};
14use crate::handlers;
15use chamber_vault::{Vault, VaultManager};
16
17pub struct ApiServer {
18 app: Router,
19 listener: TcpListener,
20}
21
22#[derive(Clone)]
23pub struct AppState {
24 pub vault: Arc<tokio::sync::Mutex<Vault>>,
25 pub vault_manager: Arc<tokio::sync::Mutex<VaultManager>>,
26 pub auth: AuthState,
27}
28
29impl ApiServer {
30 pub async fn new(vault: Vault, vault_manager: VaultManager, bind_address: &str) -> color_eyre::Result<Self> {
35 let state = AppState {
36 vault: Arc::new(tokio::sync::Mutex::new(vault)),
37 vault_manager: Arc::new(tokio::sync::Mutex::new(vault_manager)),
38 auth: AuthState::new(),
39 };
40
41 let app = build_router(Arc::new(state))?;
42
43 let listener = TcpListener::bind(bind_address).await?;
44 info!("API server will bind to: {}", bind_address);
45
46 Ok(Self { app, listener })
47 }
48
49 pub async fn serve(self) -> color_eyre::Result<()> {
54 let addr = self.listener.local_addr()?;
55 info!("API server listening on http://{}", addr);
56 warn!("API server is running on localhost only - not accessible from other machines");
57
58 axum::serve(self.listener, self.app).await?;
59 Ok(())
60 }
61
62 pub fn local_addr(&self) -> Result<std::net::SocketAddr, std::io::Error> {
66 self.listener.local_addr()
67 }
68}
69
70pub fn build_router(state: Arc<AppState>) -> color_eyre::Result<Router> {
74 let cors = CorsLayer::new()
75 .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::PATCH])
76 .allow_headers([CONTENT_TYPE])
77 .allow_origin(Any);
78
79 let app = Router::new()
80 .route("/api/v1/health", get(handlers::health))
81 .route("/api/v1/auth/login", post(handlers::login))
83 .route("/api/v1/session/unlock", post(handlers::session_unlock))
84 .route("/api/v1/auth/logout", post(handlers::logout))
86 .route("/api/v1/session/lock", post(handlers::session_lock))
87 .route("/api/v1/items", get(handlers::list_items))
89 .route("/api/v1/items", post(handlers::create_item))
90 .route("/api/v1/items/search", get(handlers::search_items))
91 .route("/api/v1/items/counts", get(handlers::get_counts))
92 .route("/api/v1/items/{id}", get(handlers::get_item))
93 .route("/api/v1/items/{id}", put(handlers::update_item))
94 .route("/api/v1/items/{id}", delete(handlers::delete_item))
95 .route("/api/v1/items/{id}/value", get(handlers::get_item_value))
96 .route("/api/v1/items/{id}/copy", post(handlers::copy_item_to_clipboard))
97 .route("/api/v1/passwords/generate", post(handlers::generate_password))
99 .route(
100 "/api/v1/passwords/memorable",
101 post(handlers::generate_memorable_password_handler),
102 )
103 .route("/api/v1/import", post(handlers::import_items_handler))
105 .route("/api/v1/export", post(handlers::export_items_handler))
106 .route("/api/v1/import/dry-run", post(handlers::dry_run_import))
107 .route("/api/v1/vaults", get(handlers::list_vaults))
109 .route("/api/v1/vaults", post(handlers::create_vault))
110 .route("/api/v1/vaults/{id}/switch", post(handlers::switch_vault))
111 .route("/api/v1/vaults/{id}", patch(handlers::update_vault))
112 .route("/api/v1/vaults/{id}", delete(handlers::delete_vault))
113 .route("/api/v1/health/report", get(handlers::health_report))
114 .route("/api/v1/stats", get(handlers::stats))
115 .layer(middleware::from_fn_with_state(Arc::clone(&state), auth_middleware))
116 .layer(cors)
117 .layer(DefaultBodyLimit::max(1024 * 1024))
118 .with_state(state);
119
120 Ok(app)
121}