refactor: comprehensive audit — fix bugs, harden security, deduplicate providers, add CI/Docker
Phase 1: Fix compilation (config_path Option<PathBuf>, streaming test, stale test cleanup) Phase 2: Fix critical bugs (remove block_on deadlocks in 4 providers, fix broken SQL query builder) Phase 3: Security hardening (session manager, real auth, token masking, Gemini key to header, password policy) Phase 4: Implement stubs (real provider test, /proc health metrics, client/provider/backup endpoints, has_images) Phase 5: Code quality (shared provider helpers, explicit re-exports, all Clippy warnings fixed, unwrap removal, 6 unused deps removed, dashboard split into 7 sub-modules) Phase 6: Infrastructure (GitHub Actions CI, multi-stage Dockerfile, rustfmt.toml, clippy.toml, script fixes)
This commit is contained in:
130
src/dashboard/auth.rs
Normal file
130
src/dashboard/auth.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use axum::{extract::State, response::Json};
|
||||
use bcrypt;
|
||||
use serde::Deserialize;
|
||||
use sqlx::Row;
|
||||
use tracing::warn;
|
||||
|
||||
use super::{ApiResponse, DashboardState};
|
||||
|
||||
// Authentication handlers
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct LoginRequest {
|
||||
pub(super) username: String,
|
||||
pub(super) password: String,
|
||||
}
|
||||
|
||||
pub(super) async fn handle_login(
|
||||
State(state): State<DashboardState>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let user_result =
|
||||
sqlx::query("SELECT username, password_hash, role, must_change_password FROM users WHERE username = ?")
|
||||
.bind(&payload.username)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => {
|
||||
let hash = row.get::<String, _>("password_hash");
|
||||
if bcrypt::verify(&payload.password, &hash).unwrap_or(false) {
|
||||
let username = row.get::<String, _>("username");
|
||||
let role = row.get::<String, _>("role");
|
||||
let must_change_password = row.get::<bool, _>("must_change_password");
|
||||
let token = state
|
||||
.session_manager
|
||||
.create_session(username.clone(), role.clone())
|
||||
.await;
|
||||
Json(ApiResponse::success(serde_json::json!({
|
||||
"token": token,
|
||||
"must_change_password": must_change_password,
|
||||
"user": {
|
||||
"username": username,
|
||||
"name": "Administrator",
|
||||
"role": role
|
||||
}
|
||||
})))
|
||||
} else {
|
||||
Json(ApiResponse::error("Invalid username or password".to_string()))
|
||||
}
|
||||
}
|
||||
Ok(None) => Json(ApiResponse::error("Invalid username or password".to_string())),
|
||||
Err(e) => {
|
||||
warn!("Database error during login: {}", e);
|
||||
Json(ApiResponse::error("Login failed due to system error".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_auth_status(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let token = headers
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "));
|
||||
|
||||
if let Some(token) = token
|
||||
&& let Some(session) = state.session_manager.validate_session(token).await
|
||||
{
|
||||
return Json(ApiResponse::success(serde_json::json!({
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"username": session.username,
|
||||
"name": "Administrator",
|
||||
"role": session.role
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
Json(ApiResponse::error("Not authenticated".to_string()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct ChangePasswordRequest {
|
||||
pub(super) current_password: String,
|
||||
pub(super) new_password: String,
|
||||
}
|
||||
|
||||
pub(super) async fn handle_change_password(
|
||||
State(state): State<DashboardState>,
|
||||
Json(payload): Json<ChangePasswordRequest>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// For now, always change 'admin' user
|
||||
let user_result = sqlx::query("SELECT password_hash FROM users WHERE username = 'admin'")
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(row) => {
|
||||
let hash = row.get::<String, _>("password_hash");
|
||||
if bcrypt::verify(&payload.current_password, &hash).unwrap_or(false) {
|
||||
let new_hash = match bcrypt::hash(&payload.new_password, 12) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Json(ApiResponse::error("Failed to hash new password".to_string())),
|
||||
};
|
||||
|
||||
let update_result = sqlx::query(
|
||||
"UPDATE users SET password_hash = ?, must_change_password = FALSE WHERE username = 'admin'",
|
||||
)
|
||||
.bind(new_hash)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match update_result {
|
||||
Ok(_) => Json(ApiResponse::success(
|
||||
serde_json::json!({ "message": "Password updated successfully" }),
|
||||
)),
|
||||
Err(e) => Json(ApiResponse::error(format!("Failed to update database: {}", e))),
|
||||
}
|
||||
} else {
|
||||
Json(ApiResponse::error("Current password incorrect".to_string()))
|
||||
}
|
||||
}
|
||||
Err(e) => Json(ApiResponse::error(format!("User not found: {}", e))),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user