feat(auth): login, roles, user mgmt, setup wizard, and session guard
- 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
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
use super::ApiError;
|
||||
|
||||
// ── GET /api/setup/status ────────────────────────────────────────────────────
|
||||
//
|
||||
// `needs_setup` is true when no user has ever been created. The frontend uses
|
||||
// this to decide whether to show the first-run setup screen.
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SetupStatus {
|
||||
pub needs_setup: bool,
|
||||
}
|
||||
|
||||
pub async fn status(State(skald): State<Arc<Skald>>) -> Result<Json<SetupStatus>, ApiError> {
|
||||
let count = skald.users().count().await?;
|
||||
Ok(Json(SetupStatus { needs_setup: count == 0 }))
|
||||
}
|
||||
|
||||
// ── POST /api/setup/user — create the first (admin) user ────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateUserBody {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub encrypted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateUserResult {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Json(body): Json<CreateUserBody>,
|
||||
) -> Result<Json<CreateUserResult>, ApiError> {
|
||||
// Guard: the setup endpoint is only available before the first user exists.
|
||||
let count = skald.users().count().await?;
|
||||
if count > 0 {
|
||||
return Err(ApiError::bad_request("setup is already complete"));
|
||||
}
|
||||
|
||||
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, None, "admin", Some(&body.password), body.encrypted)
|
||||
.await?;
|
||||
|
||||
Ok(Json(CreateUserResult { user_id: id }))
|
||||
}
|
||||
Reference in New Issue
Block a user