feat: implement initial user authentication, session management, and admin dashboard routing with
This commit is contained in:
142
src/handlers/admin.rs
Normal file
142
src/handlers/admin.rs
Normal 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, ¤t_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, ¤t_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, ¤t_user, Some("Permission denied")).await;
|
||||
}
|
||||
|
||||
if current_user.id == id {
|
||||
return render_dashboard(&state, ¤t_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, ¤t_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())
|
||||
}
|
||||
Reference in New Issue
Block a user