use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use serde_json::Value;
use crate::admin;
use crate::admin::password::PasswordContext;
use crate::admin::users::{self, UserError};
use crate::sqlite::admin_session::AdminSession;
use crate::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::mfa::verify_current_password;
use crate::webadmin::handlers::paging::{PageParams, page_envelope};
use crate::webadmin::session::{AdminClientIp, Authenticated, SelfServiceWrite, clearing_cookie};
#[derive(Debug, Deserialize)]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
}
pub async fn change_password(
State(state): State<AdminState>,
AdminClientIp(client): AdminClientIp,
headers: axum::http::HeaderMap,
SelfServiceWrite(auth): SelfServiceWrite,
request_context: crate::audit::RequestContext,
Json(body): Json<ChangePasswordRequest>,
) -> Result<Response, AdminError> {
let mut user = auth.user;
verify_current_password(&user, &body.current_password, client, &state.logins)?;
let context = PasswordContext::from_config(&state.config, &user.username);
users::change_own_password(
&mut user,
&body.new_password,
&context,
&auth.session.token_hash,
state.database.clone(),
)
.await
.map_err(|error| match error {
UserError::Policy(message) | UserError::InvalidContact(message) => {
AdminError::bad_request(message)
}
UserError::Database(_) | UserError::DuplicateUsername(_) => AdminError::internal(),
})?;
state
.record_credential_change(
&request_context,
&user.username,
&user,
crate::notify::AdminCredentialChange::Password,
true,
client,
crate::webadmin::user_agent_of(&headers),
)
.await;
Ok(StatusCode::NO_CONTENT.into_response())
}
pub async fn list_own_sessions(
State(state): State<AdminState>,
Query(params): Query<PageParams>,
auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let page = params.resolve(&state.config);
let (sessions, total) =
AdminSession::search(Some(auth.user.id), page.limit, page.offset, &state.database).await?;
let items: Vec<Value> = sessions
.iter()
.map(|session| admin::render_admin_session_detail_json(session, &auth.session.token_hash))
.collect();
Ok(Json(page_envelope(items, total, page)))
}
pub async fn revoke_own_session(
State(state): State<AdminState>,
Path(id): Path<String>,
SelfServiceWrite(auth): SelfServiceWrite,
request_context: crate::audit::RequestContext,
) -> Result<Response, AdminError> {
let session = AdminSession::find_by_user_and_fingerprint(auth.user.id, &id, &state.database)
.await?
.ok_or_else(|| session_not_found(&id))?;
let was_current = session.token_hash == auth.session.token_hash;
AdminSession::delete(&session.token_hash, &state.database).await?;
let scope = if was_current {
crate::audit::admin::SessionScope::OwnCurrent
} else {
crate::audit::admin::SessionScope::OwnOther
};
state
.record_admin_action(&request_context, &auth.user.username, |actor, ctx| {
crate::audit::admin::session_revoked(actor, ctx, scope, 1)
})
.await;
tracing::info!(event = "admin_session_revoked",
outcome = "success",
surface = "api",
scope = "self",
username = %auth.user.username,
session_fp = %id);
if was_current {
return Ok((
StatusCode::NO_CONTENT,
[(axum::http::header::SET_COOKIE, clearing_cookie())],
)
.into_response());
}
Ok(StatusCode::NO_CONTENT.into_response())
}
fn session_not_found(id: &str) -> AdminError {
AdminError::not_found(format!("no such session: {id}"))
}