- Extract scan.rs, files.rs, types.rs, processing.rs, visual_chunk_search.rs - Move AppState and AppConfig to types.rs - Each module exposes pub fn xxx_routes() -> Router<AppState> - server.rs reduced from 5005 to 118 lines (orchestrator only) - All stubs filled with real implementations from git history - Verify: cargo check, clippy, tests all pass
140 lines
3.9 KiB
Rust
140 lines
3.9 KiB
Rust
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<String> = 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<String>,
|
|
api_key: Option<String>,
|
|
user: Option<UserInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct UserInfo {
|
|
username: String,
|
|
}
|
|
|
|
async fn login(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<LoginRequest>,
|
|
) -> Result<axum::response::Response<axum::body::Body>, (StatusCode, Json<serde_json::Value>)> {
|
|
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<AppState>,
|
|
headers: axum::http::HeaderMap,
|
|
) -> Json<serde_json::value::Value> {
|
|
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<AppState> {
|
|
Router::new()
|
|
.route("/api/v1/auth/login", post(login))
|
|
.route("/api/v1/auth/logout", post(logout))
|
|
}
|