//! App passwords: per-client credentials for WebDAV mounts. //! //! One exists so a mount never carries the account password, and so an //! account that requires a passkey in the browser can be mounted at all. //! HTTP Basic can only send a password, and [`crate::api::dav`] refuses to //! quietly downgrade that requirement. //! //! A self-service password change leaves app passwords standing, exactly as //! it leaves the account's passkeys standing. Only an admin reset sweeps //! them: that one exists to lock a stranger out. For the same reason these //! routes do not drop the account's other sessions, which every route in //! [`crate::api::passkeys`] does. use std::sync::Arc; use api_types::{AppPasswordInfo, CreateAppPassword, NewAppPassword, OkResp}; use axum::Json; use axum::extract::{Path as AxumPath, State}; use axum::http::StatusCode; use crate::api::common::{SessionUser, credential_label}; use crate::auth; use crate::db::AppPasswordRow; use crate::error::{ApiError, AppState}; fn info(row: AppPasswordRow) -> AppPasswordInfo { AppPasswordInfo { id: row.id, name: row.name, created_at: row.created_at, last_used_at: row.last_used_at, } } /// GET `{AUTH_APP_PASSWORDS}`. pub async fn list( State(state): State>, SessionUser { user, .. }: SessionUser, ) -> Result>, ApiError> { let rows = state.db.app_passwords(user.id).await?; Ok(Json(rows.into_iter().map(info).collect())) } /// POST `{AUTH_APP_PASSWORDS}` — create one and return its secret. pub async fn create( State(state): State>, SessionUser { user, .. }: SessionUser, Json(req): Json, ) -> Result, ApiError> { let secret = auth::app_password(); let row = state .db .add_app_password( user.id, &credential_label(&req.name, "App password"), &auth::app_password_hash(&secret), ) .await?; let Some(row) = row else { return Err(ApiError::localized( StatusCode::BAD_REQUEST, "this account already holds as many app passwords as it may", "err_app_password_limit", )); }; tracing::info!(user = %user.name, name = %row.name, "app password created"); Ok(Json(NewAppPassword { info: info(row), secret, })) } /// DELETE `{AUTH_APP_PASSWORDS}/{id}`. pub async fn delete( State(state): State>, SessionUser { user, .. }: SessionUser, AxumPath(id): AxumPath, ) -> Result, ApiError> { if !state.db.delete_app_password(id, user.id).await? { return Err(ApiError::localized( StatusCode::NOT_FOUND, "no such app password", "err_app_password_not_found", )); } Ok(Json(OkResp {})) }