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::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::Caller;
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};
use acme_proxy_store::admin_session::AdminSession;
#[derive(Debug, Deserialize)]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct ChangeContactRequest {
#[serde(default)]
pub current_password: String,
#[serde(default)]
pub contact: Option<String>,
}
pub async fn change_contact(
State(state): State<AdminState>,
AdminClientIp(client): AdminClientIp,
SelfServiceWrite(auth): SelfServiceWrite,
request_context: acme_proxy_core::audit::RequestContext,
body: Option<Json<ChangeContactRequest>>,
) -> Result<Response, AdminError> {
let body = body.unwrap_or_default();
let caller = auth.user;
verify_current_password(&caller, &body.current_password, client, &state.logins).await?;
let mut target = caller.clone();
super::operators::apply_contact_change(
&state,
&caller,
&mut target,
body.contact.as_deref(),
client,
&request_context,
"api",
)
.await?;
Ok(StatusCode::NO_CONTENT.into_response())
}
pub async fn change_password(
State(state): State<AdminState>,
AdminClientIp(client): AdminClientIp,
SelfServiceWrite(auth): SelfServiceWrite,
request_context: acme_proxy_core::audit::RequestContext,
Json(body): Json<ChangePasswordRequest>,
) -> Result<Response, AdminError> {
let mut user = auth.user;
change_own_password_for(
&state,
&request_context,
&mut user,
&body.current_password,
&body.new_password,
&auth.session.token_hash,
client,
)
.await?;
Ok(StatusCode::NO_CONTENT.into_response())
}
pub(crate) async fn change_own_password_for(
state: &AdminState,
request: &acme_proxy_core::audit::RequestContext,
user: &mut acme_proxy_store::admin_user::AdminUser,
current_password: &str,
new_password: &str,
keep: &str,
client: Option<std::net::IpAddr>,
) -> Result<(), AdminError> {
verify_current_password(user, current_password, client, &state.logins).await?;
let context = PasswordContext::from_config(&state.config, &user.username);
users::change_own_password(user, new_password, &context, keep, 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,
&user.username,
user,
crate::webadmin::CredentialChange::Password,
true,
client,
)
.await;
Ok(())
}
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: acme_proxy_core::audit::RequestContext,
) -> Result<Response, AdminError> {
let was_current =
apply_revoke_own_session(&state, &Caller::api(&auth, &request_context), &id).await?;
if was_current {
return Ok((
StatusCode::NO_CONTENT,
[(axum::http::header::SET_COOKIE, clearing_cookie())],
)
.into_response());
}
Ok(StatusCode::NO_CONTENT.into_response())
}
pub(crate) async fn apply_revoke_own_session(
state: &AdminState,
caller: &Caller<'_>,
id: &str,
) -> Result<bool, AdminError> {
let session =
AdminSession::find_by_user_and_fingerprint(caller.auth.user.id, id, &state.database)
.await?
.ok_or_else(|| session_not_found(id))?;
let was_current = session.token_hash == caller.auth.session.token_hash;
AdminSession::delete(&session.token_hash, &state.database).await?;
let scope = if was_current {
acme_proxy_jobs::auditor::admin::SessionScope::OwnCurrent
} else {
acme_proxy_jobs::auditor::admin::SessionScope::OwnOther
};
state
.record_admin_action(caller.request, caller.username(), |actor, ctx| {
acme_proxy_jobs::auditor::admin::session_revoked(actor, ctx, scope, 1)
})
.await;
tracing::info!(event = "admin_session_revoked",
outcome = "success",
surface = caller.surface,
scope = "self",
username = %caller.username(),
session_fp = %id);
Ok(was_current)
}
fn session_not_found(id: &str) -> AdminError {
AdminError::not_found(crate::admin::subject::Subject::Session.missing(id))
}