acme_proxy_admin/webadmin/handlers/
account.rs1use axum::Json;
13use axum::extract::{Path, Query, State};
14use axum::http::StatusCode;
15use axum::response::{IntoResponse, Response};
16use serde::Deserialize;
17use serde_json::Value;
18
19use crate::admin;
20use crate::admin::password::PasswordContext;
21use crate::admin::users::{self, UserError};
22use crate::webadmin::AdminState;
23use crate::webadmin::error::AdminError;
24use crate::webadmin::handlers::Caller;
25use crate::webadmin::handlers::mfa::verify_current_password;
26use crate::webadmin::handlers::paging::{PageParams, page_envelope};
27use crate::webadmin::session::{AdminClientIp, Authenticated, SelfServiceWrite, clearing_cookie};
28use acme_proxy_store::admin_session::AdminSession;
29
30#[derive(Debug, Deserialize)]
33pub struct ChangePasswordRequest {
34 pub current_password: String,
35 pub new_password: String,
36}
37
38#[derive(Debug, Default, Deserialize)]
41pub struct ChangeContactRequest {
42 #[serde(default)]
43 pub current_password: String,
44 #[serde(default)]
45 pub contact: Option<String>,
46}
47
48pub async fn change_contact(
58 State(state): State<AdminState>,
59 AdminClientIp(client): AdminClientIp,
60 SelfServiceWrite(auth): SelfServiceWrite,
61 request_context: acme_proxy_core::audit::RequestContext,
62 body: Option<Json<ChangeContactRequest>>,
63) -> Result<Response, AdminError> {
64 let body = body.unwrap_or_default();
65 let caller = auth.user;
66 verify_current_password(&caller, &body.current_password, client, &state.logins).await?;
67
68 let mut target = caller.clone();
69 super::operators::apply_contact_change(
70 &state,
71 &caller,
72 &mut target,
73 body.contact.as_deref(),
74 client,
75 &request_context,
76 "api",
77 )
78 .await?;
79 Ok(StatusCode::NO_CONTENT.into_response())
80}
81
82pub async fn change_password(
91 State(state): State<AdminState>,
92 AdminClientIp(client): AdminClientIp,
93 SelfServiceWrite(auth): SelfServiceWrite,
94 request_context: acme_proxy_core::audit::RequestContext,
95 Json(body): Json<ChangePasswordRequest>,
96) -> Result<Response, AdminError> {
97 let mut user = auth.user;
98 change_own_password_for(
99 &state,
100 &request_context,
101 &mut user,
102 &body.current_password,
103 &body.new_password,
104 &auth.session.token_hash,
105 client,
106 )
107 .await?;
108
109 Ok(StatusCode::NO_CONTENT.into_response())
110}
111
112pub(crate) async fn change_own_password_for(
120 state: &AdminState,
121 request: &acme_proxy_core::audit::RequestContext,
122 user: &mut acme_proxy_store::admin_user::AdminUser,
123 current_password: &str,
124 new_password: &str,
125 keep: &str,
126 client: Option<std::net::IpAddr>,
127) -> Result<(), AdminError> {
128 verify_current_password(user, current_password, client, &state.logins).await?;
129
130 let context = PasswordContext::from_config(&state.config, &user.username);
131 users::change_own_password(user, new_password, &context, keep, state.database.clone())
132 .await
133 .map_err(|error| match error {
134 UserError::Policy(message) | UserError::InvalidContact(message) => {
138 AdminError::bad_request(message)
139 }
140 UserError::Database(_) | UserError::DuplicateUsername(_) => AdminError::internal(),
141 })?;
142
143 state
144 .record_credential_change(
145 request,
146 &user.username,
147 user,
148 crate::webadmin::CredentialChange::Password,
149 true,
150 client,
151 )
152 .await;
153 Ok(())
154}
155
156pub async fn list_own_sessions(
166 State(state): State<AdminState>,
167 Query(params): Query<PageParams>,
168 auth: Authenticated,
169) -> Result<Json<Value>, AdminError> {
170 let page = params.resolve(&state.config);
171 let (sessions, total) =
172 AdminSession::search(Some(auth.user.id), page.limit, page.offset, &state.database).await?;
173 let items: Vec<Value> = sessions
174 .iter()
175 .map(|session| admin::render_admin_session_detail_json(session, &auth.session.token_hash))
176 .collect();
177 Ok(Json(page_envelope(items, total, page)))
178}
179
180pub async fn revoke_own_session(
194 State(state): State<AdminState>,
195 Path(id): Path<String>,
196 SelfServiceWrite(auth): SelfServiceWrite,
197 request_context: acme_proxy_core::audit::RequestContext,
198) -> Result<Response, AdminError> {
199 let was_current =
200 apply_revoke_own_session(&state, &Caller::api(&auth, &request_context), &id).await?;
201
202 if was_current {
203 return Ok((
204 StatusCode::NO_CONTENT,
205 [(axum::http::header::SET_COOKIE, clearing_cookie())],
206 )
207 .into_response());
208 }
209 Ok(StatusCode::NO_CONTENT.into_response())
210}
211
212pub(crate) async fn apply_revoke_own_session(
217 state: &AdminState,
218 caller: &Caller<'_>,
219 id: &str,
220) -> Result<bool, AdminError> {
221 let session =
222 AdminSession::find_by_user_and_fingerprint(caller.auth.user.id, id, &state.database)
223 .await?
224 .ok_or_else(|| session_not_found(id))?;
225 let was_current = session.token_hash == caller.auth.session.token_hash;
226 AdminSession::delete(&session.token_hash, &state.database).await?;
227
228 let scope = if was_current {
229 acme_proxy_jobs::auditor::admin::SessionScope::OwnCurrent
230 } else {
231 acme_proxy_jobs::auditor::admin::SessionScope::OwnOther
232 };
233 state
234 .record_admin_action(caller.request, caller.username(), |actor, ctx| {
235 acme_proxy_jobs::auditor::admin::session_revoked(actor, ctx, scope, 1)
236 })
237 .await;
238 tracing::info!(event = "admin_session_revoked",
239 outcome = "success",
240 surface = caller.surface,
241 scope = "self",
242 username = %caller.username(),
243 session_fp = %id);
244 Ok(was_current)
245}
246
247fn session_not_found(id: &str) -> AdminError {
248 AdminError::not_found(crate::admin::subject::Subject::Session.missing(id))
249}