use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use super::middleware::extract_cookies; use super::types::AppState; static DEMO_USER_API_KEY: Lazy = Lazy::new(|| { std::env::var("MOMENTRY_DEMO_API_KEY") .unwrap_or_else(|_| "muser_demo_key_32chars_abcdef1234567890".to_string()) }); #[derive(Debug, Deserialize)] struct LoginRequest { username: String, password: String, } #[derive(Debug, Serialize)] struct LoginResponse { success: bool, message: Option, api_key: Option, user: Option, } #[derive(Debug, Serialize)] struct UserInfo { username: String, } async fn login( State(state): State, Json(req): Json, ) -> Result, (StatusCode, Json)> { let (user_id, username, role) = 'resolve: { if let Ok(Some((uid, uname, pw_hash, role_str))) = state.db.get_user_by_username(&req.username).await { if crate::core::auth::password::verify_password(&req.password, &pw_hash) { break 'resolve (uid, uname, role_str); } tracing::debug!( "[LOGIN] Local password mismatch for {}, trying SFTPGo", &req.username ); } if req.username == "demo" && req.password == "demo" { let uid = state .db .get_user_by_username("demo") .await .ok() .flatten() .map(|(id, _, _, _)| id) .unwrap_or(0); break 'resolve (uid, "demo".to_string(), "user".to_string()); } return Err(( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "success": false, "message": "Invalid username or password" })), )); }; let jwt_token = crate::core::auth::jwt::create_jwt(user_id, &username, &role).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "message": format!("JWT creation failed: {}", e) })), ) })?; let session_id = uuid::Uuid::new_v4().to_string().replace('-', ""); state .db .create_session(&session_id, user_id, &DEMO_USER_API_KEY, 24) .await .ok(); if user_id > 0 { state.db.update_last_login(user_id).await.ok(); } let body = serde_json::json!({ "success": true, "jwt": jwt_token, "api_key": DEMO_USER_API_KEY.clone(), "user": { "username": username, "role": role }, "expires_at": (chrono::Utc::now() + chrono::Duration::hours(24)).to_rfc3339() }); let json_body = axum::body::Body::from(serde_json::to_string(&body).unwrap_or_default()); let response = axum::response::Response::builder() .header("Content-Type", "application/json") .header( "Set-Cookie", format!( "session_id={}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400", session_id ), ) .body(json_body) .unwrap(); Ok(response) } async fn logout( State(state): State, headers: axum::http::HeaderMap, ) -> Json { let cookies = extract_cookies(&headers); if let Some(sid) = cookies .iter() .find(|(k, _)| k == "session_id") .map(|(_, v)| v.clone()) { state.db.delete_session(&sid).await.ok(); } Json(serde_json::json!({ "success": true, "message": "Logged out" })) } pub fn auth_routes() -> Router { Router::new() .route("/api/v1/auth/login", post(login)) .route("/api/v1/auth/logout", post(logout)) }