feat: implement initial user authentication, session management, and admin dashboard routing with

This commit is contained in:
2026-03-03 15:55:26 +00:00
parent 02709fbea1
commit ba199b8bbe
16 changed files with 1419 additions and 33 deletions

142
src/handlers/admin.rs Normal file
View File

@@ -0,0 +1,142 @@
use askama::Template;
use axum::{
extract::{Extension, Form, Path, State},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Router,
};
use bcrypt::{hash, DEFAULT_COST};
use serde::Deserialize;
use std::sync::Arc;
use crate::models::{Role, User};
use crate::utils::HtmlTemplate;
use crate::AppState;
use crate::error::AppError;
use anyhow::Context;
#[derive(Template)]
#[template(path = "dashboard.html")]
pub struct DashboardTemplate<'a> {
pub current_user: &'a User,
pub users: Vec<User>,
pub error: Option<&'a str>,
}
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/", get(dashboard))
.route("/users/add", post(add_user))
.route("/users/delete/{id}", post(delete_user))
.route("/users/password/{id}", post(change_password))
}
pub async fn dashboard(
State(state): State<Arc<AppState>>,
Extension(current_user): Extension<User>,
) -> Result<Response, AppError> {
render_dashboard(&state, &current_user, None).await
}
async fn render_dashboard(
state: &AppState,
current_user: &User,
error: Option<&str>,
) -> Result<Response, AppError> {
let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
.fetch_all(&state.db)
.await
.context("Failed to fetch users from database in render_dashboard")?;
Ok(HtmlTemplate(DashboardTemplate {
current_user,
users,
error,
})
.into_response())
}
#[derive(Deserialize)]
pub struct AddUserPayload {
pub username: String,
pub password: Option<String>,
pub role: String,
}
pub async fn add_user(
State(state): State<Arc<AppState>>,
Extension(current_user): Extension<User>,
Form(payload): Form<AddUserPayload>,
) -> Result<Response, AppError> {
if current_user.role() != Role::Admin {
return render_dashboard(&state, &current_user, Some("Permission denied")).await;
}
let password = payload.password.unwrap_or_else(|| "password".to_string());
let hashed = hash(&password, DEFAULT_COST).context("Failed to hash password")?;
let role = if payload.role == "admin" {
"admin"
} else {
"readonly"
};
sqlx::query("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")
.bind(&payload.username)
.bind(&hashed)
.bind(role)
.execute(&state.db)
.await
.context("Failed to insert new user into database")?;
Ok(Redirect::temporary("/__dungeon").into_response())
}
pub async fn delete_user(
State(state): State<Arc<AppState>>,
Extension(current_user): Extension<User>,
Path(id): Path<i64>,
) -> Result<Response, AppError> {
if current_user.role() != Role::Admin {
return render_dashboard(&state, &current_user, Some("Permission denied")).await;
}
if current_user.id == id {
return render_dashboard(&state, &current_user, Some("Cannot delete yourself")).await;
}
sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(&state.db)
.await
.context(format!("Failed to delete user with id {}", id))?;
Ok(Redirect::temporary("/__dungeon").into_response())
}
#[derive(Deserialize)]
pub struct ChangePasswordPayload {
pub password: String,
}
pub async fn change_password(
State(state): State<Arc<AppState>>,
Extension(current_user): Extension<User>,
Path(id): Path<i64>,
Form(payload): Form<ChangePasswordPayload>,
) -> Result<Response, AppError> {
if current_user.role() != Role::Admin {
return render_dashboard(&state, &current_user, Some("Permission denied")).await;
}
let hashed = hash(&payload.password, DEFAULT_COST).context("Failed to hash password")?;
sqlx::query("UPDATE users SET password_hash = ? WHERE id = ?")
.bind(&hashed)
.bind(id)
.execute(&state.db)
.await
.context(format!("Failed to update password for user with id {}", id))?;
Ok(Redirect::temporary("/__dungeon").into_response())
}