7dd77d4ef4
- New skald-setup crate: interactive first-run wizard that creates the admin user, prompts for encryption choice and password - Auth system: session-based login/logout with cookie, guard middleware - Roles API: CRUD for data-driven roles, seeded on first boot - Users management API: create, list, edit, delete users - Setup state API: check if first admin has been created - Frontend: login-page, setup-page, users-page, roles-page, profile-page components with corresponding CSS - Topbar: avatar dropdown with profile link and logout - Sidebar: nav entries for Users and Roles (admin only) - Page shell CSS: layout support for the new pages - build.sh: builds both skald and skald-setup binaries - run.sh: runs skald-setup before the server loop - CLAUDE.md: updated workspace layout and build/run docs
121 lines
4.1 KiB
Rust
121 lines
4.1 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::{Json, extract::{Path, State}};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use skald_core::db::users::UserSummary;
|
|
use skald_core::skald::Skald;
|
|
|
|
use super::ApiError;
|
|
|
|
// ── GET /api/users ───────────────────────────────────────────────────────────
|
|
|
|
pub async fn list(State(skald): State<Arc<Skald>>) -> Result<Json<Vec<UserSummary>>, ApiError> {
|
|
let users = skald.users().list().await?;
|
|
Ok(Json(users))
|
|
}
|
|
|
|
// ── POST /api/users ──────────────────────────────────────────────────────────
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CreateUserBody {
|
|
pub username: String,
|
|
pub display_name: Option<String>,
|
|
pub role_id: String,
|
|
pub password: String,
|
|
#[serde(default)]
|
|
pub encrypted: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct CreatedUser {
|
|
pub id: String,
|
|
}
|
|
|
|
pub async fn create(
|
|
State(skald): State<Arc<Skald>>,
|
|
Json(body): Json<CreateUserBody>,
|
|
) -> Result<Json<CreatedUser>, ApiError> {
|
|
let username = body.username.trim();
|
|
if username.is_empty() {
|
|
return Err(ApiError::bad_request("username must not be empty"));
|
|
}
|
|
if body.password.is_empty() {
|
|
return Err(ApiError::bad_request("password must not be empty"));
|
|
}
|
|
let id = skald
|
|
.users()
|
|
.register_user(username, body.display_name.as_deref(), &body.role_id, Some(&body.password), body.encrypted)
|
|
.await?;
|
|
Ok(Json(CreatedUser { id }))
|
|
}
|
|
|
|
// ── PUT /api/users/:id ───────────────────────────────────────────────────────
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UpdateUserBody {
|
|
pub username: String,
|
|
pub display_name: Option<String>,
|
|
pub role_id: String,
|
|
#[serde(default)]
|
|
pub active: bool,
|
|
}
|
|
|
|
pub async fn update(
|
|
State(skald): State<Arc<Skald>>,
|
|
Path(id): Path<String>,
|
|
Json(body): Json<UpdateUserBody>,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
let username = body.username.trim();
|
|
if username.is_empty() {
|
|
return Err(ApiError::bad_request("username must not be empty"));
|
|
}
|
|
skald_core::db::users::update_profile(
|
|
skald.db(),
|
|
&id,
|
|
username,
|
|
body.display_name.as_deref(),
|
|
&body.role_id,
|
|
)
|
|
.await?;
|
|
|
|
// active is separate because it's a boolean flip
|
|
skald_core::db::users::set_active(skald.db(), &id, body.active).await?;
|
|
|
|
Ok(Json(serde_json::json!({ "ok": true })))
|
|
}
|
|
|
|
// ── DELETE /api/users/:id ────────────────────────────────────────────────────
|
|
|
|
pub async fn delete(
|
|
State(skald): State<Arc<Skald>>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
skald.users().delete_user(&id).await?;
|
|
Ok(Json(serde_json::json!({ "ok": true })))
|
|
}
|
|
|
|
// ── POST /api/users/:id/password ─────────────────────────────────────────────
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct ResetPasswordBody {
|
|
pub password: String,
|
|
}
|
|
|
|
pub async fn reset_password(
|
|
State(skald): State<Arc<Skald>>,
|
|
Path(id): Path<String>,
|
|
Json(body): Json<ResetPasswordBody>,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
if body.password.is_empty() {
|
|
return Err(ApiError::bad_request("password must not be empty"));
|
|
}
|
|
// change_password with no old password and a new one resets it. For
|
|
// encrypted users the admin must supply the old password; this endpoint
|
|
// is for cleartext users or for admin-initiated resets of cleartext
|
|
// accounts. For encrypted users, the user should change their own password.
|
|
skald.users().change_password(&id, None, Some(&body.password)).await
|
|
.map_err(|e| ApiError::bad_request(e.to_string()))?;
|
|
Ok(Json(serde_json::json!({ "ok": true })))
|
|
}
|