app_passwords.rs
⎇
Raw
1//! App passwords: per-client credentials for WebDAV mounts.
2//!
3//! One exists so a mount never carries the account password, and so an
4//! account that requires a passkey in the browser can be mounted at all.
5//! HTTP Basic can only send a password, and [`crate::api::dav`] refuses to
6//! quietly downgrade that requirement.
7//!
8//! A self-service password change leaves app passwords standing, exactly as
9//! it leaves the account's passkeys standing. Only an admin reset sweeps
10//! them: that one exists to lock a stranger out. For the same reason these
11//! routes do not drop the account's other sessions, which every route in
12//! [`crate::api::passkeys`] does.
13
14use std::sync::Arc;
15
16use api_types::{AppPasswordInfo, CreateAppPassword, NewAppPassword, OkResp};
17use axum::Json;
18use axum::extract::{Path as AxumPath, State};
19use axum::http::StatusCode;
20
21use crate::api::common::{SessionUser, credential_label};
22use crate::auth;
23use crate::error::{ApiError, AppState};
24
25/// GET `{AUTH_APP_PASSWORDS}`.
26pub async fn list(
27 State(state): State<Arc<AppState>>,
28 SessionUser { user, .. }: SessionUser,
29) -> Result<Json<Vec<AppPasswordInfo>>, ApiError> {
30 Ok(Json(state.db.app_passwords(user.id).await?))
31}
32
33/// POST `{AUTH_APP_PASSWORDS}` — create one and return its secret.
34pub async fn create(
35 State(state): State<Arc<AppState>>,
36 SessionUser { user, .. }: SessionUser,
37 Json(req): Json<CreateAppPassword>,
38) -> Result<Json<NewAppPassword>, ApiError> {
39 let secret = auth::short_token();
40 let row = state
41 .db
42 .add_app_password(
43 user.id,
44 &credential_label(&req.name, "App password"),
45 &auth::app_password_hash(&secret),
46 )
47 .await?;
48 let Some(row) = row else {
49 return Err(ApiError::localized(
50 StatusCode::BAD_REQUEST,
51 "this account already holds as many app passwords as it may",
52 "err_app_password_limit",
53 ));
54 };
55 tracing::info!(user = %user.name, name = %row.name, "app password created");
56 Ok(Json(NewAppPassword { info: row, secret }))
57}
58
59/// DELETE `{AUTH_APP_PASSWORDS}/{id}`.
60pub async fn delete(
61 State(state): State<Arc<AppState>>,
62 SessionUser { user, .. }: SessionUser,
63 AxumPath(id): AxumPath<i64>,
64) -> Result<Json<OkResp>, ApiError> {
65 if !state.db.delete_app_password(id, user.id).await? {
66 return Err(ApiError::localized(
67 StatusCode::NOT_FOUND,
68 "no such app password",
69 "err_app_password_not_found",
70 ));
71 }
72 Ok(Json(OkResp {}))
73}
74