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::db::AppPasswordRow;
24use crate::error::{ApiError, AppState};
25
26fn info(row: AppPasswordRow) -> AppPasswordInfo {
27 AppPasswordInfo {
28 id: row.id,
29 name: row.name,
30 created_at: row.created_at,
31 last_used_at: row.last_used_at,
32 }
33}
34
35/// GET `{AUTH_APP_PASSWORDS}`.
36pub async fn list(
37 State(state): State<Arc<AppState>>,
38 SessionUser { user, .. }: SessionUser,
39) -> Result<Json<Vec<AppPasswordInfo>>, ApiError> {
40 let rows = state.db.app_passwords(user.id).await?;
41 Ok(Json(rows.into_iter().map(info).collect()))
42}
43
44/// POST `{AUTH_APP_PASSWORDS}` — create one and return its secret.
45pub async fn create(
46 State(state): State<Arc<AppState>>,
47 SessionUser { user, .. }: SessionUser,
48 Json(req): Json<CreateAppPassword>,
49) -> Result<Json<NewAppPassword>, ApiError> {
50 let secret = auth::app_password();
51 let row = state
52 .db
53 .add_app_password(
54 user.id,
55 &credential_label(&req.name, "App password"),
56 &auth::app_password_hash(&secret),
57 )
58 .await?;
59 let Some(row) = row else {
60 return Err(ApiError::localized(
61 StatusCode::BAD_REQUEST,
62 "this account already holds as many app passwords as it may",
63 "err_app_password_limit",
64 ));
65 };
66 tracing::info!(user = %user.name, name = %row.name, "app password created");
67 Ok(Json(NewAppPassword {
68 info: info(row),
69 secret,
70 }))
71}
72
73/// DELETE `{AUTH_APP_PASSWORDS}/{id}`.
74pub async fn delete(
75 State(state): State<Arc<AppState>>,
76 SessionUser { user, .. }: SessionUser,
77 AxumPath(id): AxumPath<i64>,
78) -> Result<Json<OkResp>, ApiError> {
79 if !state.db.delete_app_password(id, user.id).await? {
80 return Err(ApiError::localized(
81 StatusCode::NOT_FOUND,
82 "no such app password",
83 "err_app_password_not_found",
84 ));
85 }
86 Ok(Json(OkResp {}))
87}
88