Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
7.3 kB · 255 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256use axum::{Json, extract::State};use serde::{Deserialize, Serialize};use serde_json::json;use tranquil_db_traits::AppPasswordCreate;use tranquil_pds::api::EmptyResponse;use tranquil_pds::api::error::{ApiError, DbResultExt};use tranquil_pds::auth::{Auth, NotTakendown, Permissive, generate_app_password};use tranquil_pds::delegation::{DelegationActionType, intersect_scopes};use tranquil_pds::rate_limit::{AppPasswordLimit, RateLimited};use tranquil_pds::state::AppState;
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct AppPassword { pub name: String, pub created_at: String, pub privileged: bool, #[serde(skip_serializing_if = "Option::is_none")] pub scopes: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub created_by_controller: Option<String>,}
#[derive(Serialize)]pub struct ListAppPasswordsOutput { pub passwords: Vec<AppPassword>,}
pub async fn list_app_passwords( State(state): State<AppState>, auth: Auth<Permissive>,) -> Result<Json<ListAppPasswordsOutput>, ApiError> { let user = state .repos .user .get_by_did(&auth.did) .await .log_db_err("getting user")? .ok_or(ApiError::AccountNotFound)?;
let rows = state .repos .session .list_app_passwords(user.id) .await .log_db_err("listing app passwords")?; let passwords: Vec<AppPassword> = rows .iter() .map(|row| AppPassword { name: row.name.clone(), created_at: row.created_at.to_rfc3339(), privileged: row.privilege.is_privileged(), scopes: row.scopes.clone(), created_by_controller: row .created_by_controller_did .as_ref() .map(|d| d.to_string()), }) .collect(); Ok(Json(ListAppPasswordsOutput { passwords }))}
#[derive(Deserialize)]pub struct CreateAppPasswordInput { pub name: String, pub privileged: Option<bool>, pub scopes: Option<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct CreateAppPasswordOutput { pub name: String, pub password: String, pub created_at: String, pub privileged: bool, #[serde(skip_serializing_if = "Option::is_none")] pub scopes: Option<String>,}
pub async fn create_app_password( State(state): State<AppState>, _rate_limit: RateLimited<AppPasswordLimit>, auth: Auth<NotTakendown>, Json(input): Json<CreateAppPasswordInput>,) -> Result<Json<CreateAppPasswordOutput>, ApiError> { let user = state .repos .user .get_by_did(&auth.did) .await .log_db_err("getting user")? .ok_or(ApiError::AccountNotFound)?;
let name = input.name.trim(); if name.is_empty() { return Err(ApiError::InvalidRequest("name is required".into())); }
if state .repos .session .get_app_password_by_name(user.id, name) .await .log_db_err("checking app password")? .is_some() { return Err(ApiError::DuplicateAppPassword); }
let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { let grant = state .repos .delegation .get_delegation(&auth.did, controller) .await .ok() .flatten(); let granted_scopes = match grant { Some(g) => g.granted_scopes, None => return Err(ApiError::InsufficientScope(None)), };
let requested = input.scopes.as_deref().unwrap_or("atproto"); let intersected = intersect_scopes(requested, granted_scopes.as_str());
if intersected.is_empty() && !granted_scopes.is_empty() { return Err(ApiError::InsufficientScope(None)); }
let scope_result = if intersected.is_empty() { None } else { Some(intersected) }; (scope_result, Some(controller.clone())) } else { let scopes = match input.scopes { Some(ref s) => s.clone(), None => match input.privileged { Some(false) => "transition:generic".to_string(), _ => "transition:generic transition:chat.bsky".to_string(), }, }; (Some(scopes), None) };
let password = generate_app_password();
let password_hash = crate::common::hash_password_async(&password).await?;
let privilege = tranquil_db_traits::AppPasswordPrivilege::from_privileged_flag( input.privileged.unwrap_or(false), ); let created_at = chrono::Utc::now();
let create_data = AppPasswordCreate { user_id: user.id, name: name.to_string(), password_hash, privilege, scopes: final_scopes.clone(), created_by_controller_did: controller_did.clone(), };
state .repos .session .create_app_password(&create_data) .await .log_db_err("creating app password")?;
if let Some(ref controller) = controller_did { let _ = state .repos .delegation .log_delegation_action( &auth.did, controller, Some(controller), DelegationActionType::AccountAction, Some(json!({ "action": "create_app_password", "name": name, "scopes": final_scopes })), None, None, ) .await; } Ok(Json(CreateAppPasswordOutput { name: name.to_string(), password: password.into_inner(), created_at: created_at.to_rfc3339(), privileged: privilege.is_privileged(), scopes: final_scopes, }))}
#[derive(Deserialize)]pub struct RevokeAppPasswordInput { pub name: String,}
pub async fn revoke_app_password( State(state): State<AppState>, auth: Auth<Permissive>, Json(input): Json<RevokeAppPasswordInput>,) -> Result<Json<EmptyResponse>, ApiError> { let user = state .repos .user .get_by_did(&auth.did) .await .log_db_err("getting user")? .ok_or(ApiError::AccountNotFound)?;
let name = input.name.trim(); if name.is_empty() { return Err(ApiError::InvalidRequest("name is required".into())); }
let sessions_to_invalidate = state .repos .session .get_session_jtis_by_app_password(&auth.did, name) .await .unwrap_or_default();
state .repos .session .delete_sessions_by_app_password(&auth.did, name) .await .log_db_err("revoking sessions for app password")?;
futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, jti); let cache = state.cache.clone(); async move { let _ = cache.delete(&cache_key).await; } })) .await;
state .repos .session .delete_app_password(user.id, name) .await .log_db_err("revoking app password")?;
Ok(Json(EmptyResponse {}))}