Our Personal Data Server from scratch!
0

Configure Feed

Select the types of activity you want to include in your feed.

tranquil-pds / crates / tranquil-api / src / server / app_password.rs
7.3 kB 251 lines
1use axum::{ 2 Json, 3 extract::State, 4 response::{IntoResponse, Response}, 5}; 6use serde::{Deserialize, Serialize}; 7use serde_json::json; 8use tracing::error; 9use tranquil_db_traits::AppPasswordCreate; 10use tranquil_pds::api::EmptyResponse; 11use tranquil_pds::api::error::{ApiError, DbResultExt}; 12use tranquil_pds::auth::{Auth, NotTakendown, Permissive, generate_app_password}; 13use tranquil_pds::delegation::{DelegationActionType, intersect_scopes}; 14use tranquil_pds::rate_limit::{AppPasswordLimit, RateLimited}; 15use tranquil_pds::state::AppState; 16 17#[derive(Serialize)] 18#[serde(rename_all = "camelCase")] 19pub struct AppPassword { 20 pub name: String, 21 pub created_at: String, 22 pub privileged: bool, 23 #[serde(skip_serializing_if = "Option::is_none")] 24 pub scopes: Option<String>, 25 #[serde(skip_serializing_if = "Option::is_none")] 26 pub created_by_controller: Option<String>, 27} 28 29#[derive(Serialize)] 30pub struct ListAppPasswordsOutput { 31 pub passwords: Vec<AppPassword>, 32} 33 34pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 auth: Auth<Permissive>, 37) -> Result<Response, ApiError> { 38 let user = state 39 .user_repo 40 .get_by_did(&auth.did) 41 .await 42 .log_db_err("getting user")? 43 .ok_or(ApiError::AccountNotFound)?; 44 45 let rows = state 46 .session_repo 47 .list_app_passwords(user.id) 48 .await 49 .log_db_err("listing app passwords")?; 50 let passwords: Vec<AppPassword> = rows 51 .iter() 52 .map(|row| AppPassword { 53 name: row.name.clone(), 54 created_at: row.created_at.to_rfc3339(), 55 privileged: row.privilege.is_privileged(), 56 scopes: row.scopes.clone(), 57 created_by_controller: row 58 .created_by_controller_did 59 .as_ref() 60 .map(|d| d.to_string()), 61 }) 62 .collect(); 63 Ok(Json(ListAppPasswordsOutput { passwords }).into_response()) 64} 65 66#[derive(Deserialize)] 67pub struct CreateAppPasswordInput { 68 pub name: String, 69 pub privileged: Option<bool>, 70 pub scopes: Option<String>, 71} 72 73#[derive(Serialize)] 74#[serde(rename_all = "camelCase")] 75pub struct CreateAppPasswordOutput { 76 pub name: String, 77 pub password: String, 78 pub created_at: String, 79 pub privileged: bool, 80 #[serde(skip_serializing_if = "Option::is_none")] 81 pub scopes: Option<String>, 82} 83 84pub async fn create_app_password( 85 State(state): State<AppState>, 86 _rate_limit: RateLimited<AppPasswordLimit>, 87 auth: Auth<NotTakendown>, 88 Json(input): Json<CreateAppPasswordInput>, 89) -> Result<Response, ApiError> { 90 let user = state 91 .user_repo 92 .get_by_did(&auth.did) 93 .await 94 .log_db_err("getting user")? 95 .ok_or(ApiError::AccountNotFound)?; 96 97 let name = input.name.trim(); 98 if name.is_empty() { 99 return Err(ApiError::InvalidRequest("name is required".into())); 100 } 101 102 if state 103 .session_repo 104 .get_app_password_by_name(user.id, name) 105 .await 106 .log_db_err("checking app password")? 107 .is_some() 108 { 109 return Err(ApiError::DuplicateAppPassword); 110 } 111 112 let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { 113 let grant = state 114 .delegation_repo 115 .get_delegation(&auth.did, controller) 116 .await 117 .ok() 118 .flatten(); 119 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default(); 120 121 let requested = input.scopes.as_deref().unwrap_or("atproto"); 122 let intersected = intersect_scopes(requested, granted_scopes.as_str()); 123 124 if intersected.is_empty() && !granted_scopes.is_empty() { 125 return Err(ApiError::InsufficientScope(None)); 126 } 127 128 let scope_result = if intersected.is_empty() { 129 None 130 } else { 131 Some(intersected) 132 }; 133 (scope_result, Some(controller.clone())) 134 } else { 135 (input.scopes.clone(), None) 136 }; 137 138 let password = generate_app_password(); 139 140 let password_clone = password.clone(); 141 let password_hash = 142 tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST)) 143 .await 144 .map_err(|e| { 145 error!("Failed to spawn blocking task: {:?}", e); 146 ApiError::InternalError(None) 147 })? 148 .map_err(|e| { 149 error!("Failed to hash password: {:?}", e); 150 ApiError::InternalError(None) 151 })?; 152 153 let privilege = tranquil_db_traits::AppPasswordPrivilege::from_privileged_flag( 154 input.privileged.unwrap_or(false), 155 ); 156 let created_at = chrono::Utc::now(); 157 158 let create_data = AppPasswordCreate { 159 user_id: user.id, 160 name: name.to_string(), 161 password_hash, 162 privilege, 163 scopes: final_scopes.clone(), 164 created_by_controller_did: controller_did.clone(), 165 }; 166 167 state 168 .session_repo 169 .create_app_password(&create_data) 170 .await 171 .log_db_err("creating app password")?; 172 173 if let Some(ref controller) = controller_did { 174 let _ = state 175 .delegation_repo 176 .log_delegation_action( 177 &auth.did, 178 controller, 179 Some(controller), 180 DelegationActionType::AccountAction, 181 Some(json!({ 182 "action": "create_app_password", 183 "name": name, 184 "scopes": final_scopes 185 })), 186 None, 187 None, 188 ) 189 .await; 190 } 191 Ok(Json(CreateAppPasswordOutput { 192 name: name.to_string(), 193 password, 194 created_at: created_at.to_rfc3339(), 195 privileged: privilege.is_privileged(), 196 scopes: final_scopes, 197 }) 198 .into_response()) 199} 200 201#[derive(Deserialize)] 202pub struct RevokeAppPasswordInput { 203 pub name: String, 204} 205 206pub async fn revoke_app_password( 207 State(state): State<AppState>, 208 auth: Auth<Permissive>, 209 Json(input): Json<RevokeAppPasswordInput>, 210) -> Result<Response, ApiError> { 211 let user = state 212 .user_repo 213 .get_by_did(&auth.did) 214 .await 215 .log_db_err("getting user")? 216 .ok_or(ApiError::AccountNotFound)?; 217 218 let name = input.name.trim(); 219 if name.is_empty() { 220 return Err(ApiError::InvalidRequest("name is required".into())); 221 } 222 223 let sessions_to_invalidate = state 224 .session_repo 225 .get_session_jtis_by_app_password(&auth.did, name) 226 .await 227 .unwrap_or_default(); 228 229 state 230 .session_repo 231 .delete_sessions_by_app_password(&auth.did, name) 232 .await 233 .log_db_err("revoking sessions for app password")?; 234 235 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 236 let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, jti); 237 let cache = state.cache.clone(); 238 async move { 239 let _ = cache.delete(&cache_key).await; 240 } 241 })) 242 .await; 243 244 state 245 .session_repo 246 .delete_app_password(user.id, name) 247 .await 248 .log_db_err("revoking app password")?; 249 250 Ok(EmptyResponse::ok().into_response()) 251}