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,192 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use skald_core::auth::COOKIE_NAME;
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
use super::guard::AuthUser;
|
||||
use super::ApiError;
|
||||
|
||||
// ── POST /api/auth/login ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginBody {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Authenticates the user, unlocks their database (pool stays open until
|
||||
/// restart), mints a session token, and sets it as an `HttpOnly` cookie.
|
||||
pub async fn login(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Json(body): Json<LoginBody>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let token = skald
|
||||
.sessions()
|
||||
.login(&body.username, &body.password)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::bad_request("Invalid username or password")
|
||||
})?;
|
||||
|
||||
let cookie = format!(
|
||||
"{COOKIE_NAME}={token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=2592000"
|
||||
);
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
[(header::SET_COOKIE, cookie)],
|
||||
Json(LoginResult { ok: true }),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
// ── GET /api/auth/me ─────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MeResponse {
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
/// Returns the authenticated user's profile, or 401 if no valid session.
|
||||
pub async fn me(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, ApiError> {
|
||||
let token = match extract_session_token(&headers) {
|
||||
Some(t) => t,
|
||||
None => return Ok(StatusCode::UNAUTHORIZED.into_response()),
|
||||
};
|
||||
|
||||
let user_id = match skald.sessions().user_of(&token) {
|
||||
Some(id) => id,
|
||||
None => return Ok(StatusCode::UNAUTHORIZED.into_response()),
|
||||
};
|
||||
|
||||
let user = skald
|
||||
.users()
|
||||
.get(&user_id)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("user not found"))?;
|
||||
|
||||
Ok(Json(MeResponse {
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
role_id: user.role_id,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
// ── POST /api/auth/logout ────────────────────────────────────────────────────
|
||||
|
||||
pub async fn logout(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
if let Some(token) = extract_session_token(&headers) {
|
||||
skald.sessions().logout(&token);
|
||||
}
|
||||
// Clear the cookie.
|
||||
let expired = format!(
|
||||
"{COOKIE_NAME}=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0"
|
||||
);
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::SET_COOKIE, expired)],
|
||||
Json(serde_json::json!({ "ok": true })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Parses the `Cookie` header and extracts the session token.
|
||||
fn extract_session_token(headers: &HeaderMap) -> Option<String> {
|
||||
let raw = headers.get(header::COOKIE)?.to_str().ok()?;
|
||||
for pair in raw.split(';') {
|
||||
let pair = pair.trim();
|
||||
if let Some(val) = pair.strip_prefix(&format!("{COOKIE_NAME}=")) {
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ── PUT /api/auth/profile — update display name ──────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateProfileBody {
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update_profile(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Extension(auth): Extension<AuthUser>,
|
||||
Json(body): Json<UpdateProfileBody>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = skald
|
||||
.users()
|
||||
.get(&auth.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("user not found"))?;
|
||||
|
||||
skald_core::db::users::rename(
|
||||
skald.db(),
|
||||
&auth.user_id,
|
||||
&user.username,
|
||||
body.display_name.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
// ── POST /api/auth/change-password ───────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangePasswordBody {
|
||||
pub current_password: Option<String>,
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Extension(auth): Extension<AuthUser>,
|
||||
Json(body): Json<ChangePasswordBody>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
if body.new_password.is_empty() {
|
||||
return Err(ApiError::bad_request("new password must not be empty"));
|
||||
}
|
||||
|
||||
skald
|
||||
.users()
|
||||
.change_password(
|
||||
&auth.user_id,
|
||||
body.current_password.as_deref(),
|
||||
Some(&body.new_password),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
skald_core::users::AuthError::WrongPassword => {
|
||||
ApiError::bad_request("Current password is incorrect")
|
||||
}
|
||||
other => ApiError::bad_request(other.to_string()),
|
||||
})?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Authentication gate for `/api` — deny by default.
|
||||
//!
|
||||
//! Applied as a layer over the whole API router, so **every** endpoint requires
|
||||
//! a valid session cookie except the handful named in [`is_public`]. A new route
|
||||
//! is therefore protected the moment it is added, with no extra step; opening it
|
||||
//! up is the deliberate act.
|
||||
//!
|
||||
//! Kept separate from the `auth` handlers on purpose: this is the cross-cutting
|
||||
//! gate, they are the login/logout endpoints. On a hit it resolves the session
|
||||
//! once and injects [`AuthUser`] into the request, so downstream handlers — and
|
||||
//! the per-user pool routing that will land with the multi-user split — read the
|
||||
//! identity without re-parsing the cookie.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::{StatusCode, header},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
use skald_core::auth::COOKIE_NAME;
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
/// The authenticated user, injected into request extensions by [`require_auth`]
|
||||
/// for every gated endpoint. This is where per-user pool routing will read the
|
||||
/// id once the database split (blueprint §5.1) reaches the call sites.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthUser {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
/// Endpoints reachable without a session. Everything else under `/api` needs a
|
||||
/// valid cookie.
|
||||
///
|
||||
/// - `auth/login` — the bootstrap: you cannot have a session before it.
|
||||
/// - `auth/logout` — idempotent; tolerating an already-expired session is kinder
|
||||
/// than answering 401 to someone trying to log out.
|
||||
/// - `auth/me` — the canonical "am I logged in?" probe; it answers 401 itself.
|
||||
/// - `setup/status` / `setup/user` — called before the first user exists, and
|
||||
/// `create_user` already refuses once one does.
|
||||
fn is_public(path: &str) -> bool {
|
||||
// The layer sits on the inner router, so the path arrives without the `/api`
|
||||
// nest prefix — but strip it defensively so a move of the layer can't
|
||||
// silently open everything up.
|
||||
let p = path.strip_prefix("/api").unwrap_or(path);
|
||||
matches!(
|
||||
p,
|
||||
"/auth/login" | "/auth/logout" | "/auth/me" | "/setup/status" | "/setup/user"
|
||||
)
|
||||
}
|
||||
|
||||
/// Extracts the session token from the `Cookie` header.
|
||||
fn session_token(headers: &axum::http::HeaderMap) -> Option<String> {
|
||||
let raw = headers.get(header::COOKIE)?.to_str().ok()?;
|
||||
raw.split(';')
|
||||
.filter_map(|pair| pair.trim().strip_prefix(&format!("{COOKIE_NAME}=")))
|
||||
.next()
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
/// The gate. Public paths pass through untouched; every other request must carry
|
||||
/// a cookie that maps to a live session, or it gets 401 before reaching a
|
||||
/// handler.
|
||||
pub async fn require_auth(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
if is_public(req.uri().path()) {
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
match session_token(req.headers()).and_then(|t| skald.sessions().user_of(&t)) {
|
||||
Some(user_id) => {
|
||||
req.extensions_mut().insert(AuthUser { user_id });
|
||||
next.run(req).await
|
||||
}
|
||||
None => StatusCode::UNAUTHORIZED.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
pub mod agents;
|
||||
pub mod auth;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod approval;
|
||||
pub mod cron;
|
||||
pub mod dev;
|
||||
pub mod file_watch;
|
||||
pub mod guard;
|
||||
pub mod stats;
|
||||
pub mod files;
|
||||
pub mod image_generate_models;
|
||||
@@ -15,12 +17,15 @@ pub mod mcp;
|
||||
pub mod mcp_media;
|
||||
pub mod plugins;
|
||||
pub mod projects;
|
||||
pub mod roles;
|
||||
pub mod run_context;
|
||||
pub mod sessions;
|
||||
pub mod setup;
|
||||
pub mod transcribe_audio;
|
||||
pub mod transcribe_models;
|
||||
pub mod tts_models;
|
||||
pub mod uploads;
|
||||
pub mod users_mgmt;
|
||||
pub mod ws;
|
||||
pub mod ws_session;
|
||||
|
||||
@@ -44,6 +49,15 @@ pub fn router() -> Router<Arc<Skald>> {
|
||||
// Custom slash commands (file-based, read-only listing for autocomplete + /help)
|
||||
.route("/commands", get(commands::list))
|
||||
.route("/sessions", get(sessions::list_sessions).post(sessions::create))
|
||||
// First-run setup
|
||||
.route("/setup/status", get(setup::status))
|
||||
.route("/setup/user", post(setup::create_user))
|
||||
// Auth
|
||||
.route("/auth/login", post(auth::login))
|
||||
.route("/auth/me", get(auth::me))
|
||||
.route("/auth/logout", post(auth::logout))
|
||||
.route("/auth/profile", put(auth::update_profile))
|
||||
.route("/auth/change-password", post(auth::change_password))
|
||||
.route("/sessions/{id}", get(sessions::get_session_detail))
|
||||
.route("/web/messages", get(sessions::web_messages))
|
||||
.route("/{source}/messages", get(sessions::source_messages))
|
||||
@@ -130,6 +144,13 @@ pub fn router() -> Router<Arc<Skald>> {
|
||||
// Plugins
|
||||
.route("/plugins", get(plugins::list))
|
||||
.route("/plugins/{id}", put(plugins::update))
|
||||
// Roles
|
||||
.route("/roles", get(roles::list).post(roles::create))
|
||||
.route("/roles/{id}", put(roles::update).delete(roles::delete))
|
||||
// User management
|
||||
.route("/users", get(users_mgmt::list).post(users_mgmt::create))
|
||||
.route("/users/{id}", put(users_mgmt::update).delete(users_mgmt::delete))
|
||||
.route("/users/{id}/password", post(users_mgmt::reset_password))
|
||||
// Images (generated by image_generate tool)
|
||||
.route("/images/{task_id}", get(images::get_image))
|
||||
// MCP tool-result media (images/audio/files returned by MCP servers)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Json, extract::{Path, State}};
|
||||
use serde::Deserialize;
|
||||
|
||||
use skald_core::db::roles::{self, ADMIN_ROLE_ID, Role};
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
use super::ApiError;
|
||||
|
||||
pub async fn list(State(skald): State<Arc<Skald>>) -> Result<Json<Vec<Role>>, ApiError> {
|
||||
let roles = roles::list(skald.db()).await?;
|
||||
Ok(Json(roles))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateBody {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub permission_group: String,
|
||||
pub attrs: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Json(body): Json<CreateBody>,
|
||||
) -> Result<Json<Role>, ApiError> {
|
||||
let id = body.id.trim();
|
||||
if id.is_empty() || id == ADMIN_ROLE_ID {
|
||||
return Err(ApiError::bad_request("invalid role id"));
|
||||
}
|
||||
if body.label.trim().is_empty() {
|
||||
return Err(ApiError::bad_request("label must not be empty"));
|
||||
}
|
||||
roles::insert(skald.db(), id, body.label.trim(), &body.permission_group, body.attrs.as_deref())
|
||||
.await?;
|
||||
let role = roles::get(skald.db(), id).await?.ok_or_else(|| ApiError::not_found("role not found after insert"))?;
|
||||
Ok(Json(role))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateBody {
|
||||
pub label: String,
|
||||
pub permission_group: String,
|
||||
pub attrs: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Path(id): Path<String>,
|
||||
Json(body): Json<UpdateBody>,
|
||||
) -> Result<Json<Role>, ApiError> {
|
||||
if id == ADMIN_ROLE_ID {
|
||||
return Err(ApiError::bad_request("the built-in admin role cannot be modified"));
|
||||
}
|
||||
let ok = roles::update(skald.db(), &id, body.label.trim(), &body.permission_group, body.attrs.as_deref())
|
||||
.await?;
|
||||
if !ok {
|
||||
return Err(ApiError::not_found("role not found"));
|
||||
}
|
||||
let role = roles::get(skald.db(), &id).await?.ok_or_else(|| ApiError::not_found("role not found after update"))?;
|
||||
Ok(Json(role))
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
if id == ADMIN_ROLE_ID {
|
||||
return Err(ApiError::bad_request("the built-in admin role cannot be deleted"));
|
||||
}
|
||||
let count = roles::user_count(skald.db(), &id).await?;
|
||||
if count > 0 {
|
||||
return Err(ApiError::bad_request(format!("{count} user(s) still assigned to this role")));
|
||||
}
|
||||
roles::delete(skald.db(), &id).await?;
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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 })))
|
||||
}
|
||||
Reference in New Issue
Block a user