use std::path::Path; use std::sync::Arc; use axum::extract::State; use axum::http::{header, HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Deserialize; use crate::auth::{self, clear_session_cookie, parse_session_cookie, session_cookie}; use crate::error::{ApiError, AppState}; #[derive(Deserialize)] pub struct CredentialsBody { pub name: String, pub password: String, } /// GET /api/auth/me /// /// - No users at all → `200 {"first_boot": true}` /// - No/invalid session → `401` /// - Valid session → user info + visible roots pub async fn me( State(state): State>, headers: HeaderMap, ) -> Result, ApiError> { if state.db.user_count().await == 0 { return Ok(Json(serde_json::json!({ "first_boot": true, "user": null, "roots": [], "allow_writable_shares": false }))); } let Some(token) = parse_session_cookie(&headers) else { return Err(ApiError::new(StatusCode::UNAUTHORIZED, "not signed in")); }; let Some(user) = state.db.session_user(&token).await else { return Err(ApiError::new( StatusCode::UNAUTHORIZED, "session expired, please sign in again", )); }; let roots = state .db .user_roots(user.id) .await .into_iter() .map(|r| { serde_json::json!({ "id": r.id, "name": display_name(&state.root, &r.path), "path": r.path, "mode": r.mode, }) }) .collect::>(); Ok(Json(serde_json::json!({ "first_boot": false, "user": { "id": user.id, "name": user.name, "is_admin": user.is_admin, }, "roots": roots, "allow_writable_shares": state.db.allow_writable_shares().await, }))) } /// Display name for a user root: the folder name, or the root folder's /// own name when the user root is the whole root ("."). fn display_name(server_root: &Path, rel: &str) -> String { let p = Path::new(rel); let name = if rel == "." { server_root.file_name() } else { p.file_name().filter(|_| !p.as_os_str().is_empty()) }; name.map(|s| s.to_string_lossy().into_owned()) .unwrap_or_else(|| rel.to_string()) } /// POST /api/auth/setup — create the first admin account. /// Only available while no users exist. pub async fn setup( State(state): State>, Json(body): Json, ) -> Result { let name = body.name.trim(); if name.is_empty() || name.len() > 64 { return Err(ApiError::new( StatusCode::BAD_REQUEST, "name must be 1–64 characters", )); } if body.password.len() < 8 { return Err(ApiError::new( StatusCode::BAD_REQUEST, "password must be at least 8 characters", )); } if state.db.user_count().await > 0 { return Err(ApiError::new( StatusCode::CONFLICT, "server is already set up", )); } let pass_hash = auth::hash_password(&body.password).map_err(|e| { ApiError::new( StatusCode::INTERNAL_SERVER_ERROR, format!("hashing failed: {e}"), ) })?; let user = state.db.create_admin(name, &pass_hash).await?; let token = auth::random_token(); state.db.create_session(user.id, &token).await?; let mut res = Json(serde_json::json!({ "ok": true })).into_response(); res.headers_mut().insert( header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap(), ); Ok(res) } /// POST /api/auth/login pub async fn login( State(state): State>, Json(body): Json, ) -> Result { let Some(user) = state.db.verify_password(&body.name, &body.password).await else { return Err(ApiError::new( StatusCode::UNAUTHORIZED, "invalid name or password", )); }; let token = auth::random_token(); state.db.create_session(user.id, &token).await?; let mut res = Json(serde_json::json!({ "ok": true })).into_response(); res.headers_mut().insert( header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap(), ); Ok(res) } /// POST /api/auth/logout pub async fn logout(State(state): State>, headers: HeaderMap) -> Response { if let Some(token) = parse_session_cookie(&headers) { let _ = state.db.delete_session(&token).await; } let mut res = Json(serde_json::json!({ "ok": true })).into_response(); res.headers_mut().insert( header::SET_COOKIE, clear_session_cookie(state.https).parse().unwrap(), ); res }