this repo has no description
1use crate::api::ApiError; 2use crate::auth::BearerAuth; 3use crate::delegation::{self, DelegationActionType}; 4use crate::state::{AppState, RateLimitKind}; 5use crate::util::get_user_id_by_did; 6use axum::{ 7 Json, 8 extract::State, 9 http::HeaderMap, 10 response::{IntoResponse, Response}, 11}; 12use serde::{Deserialize, Serialize}; 13use serde_json::json; 14use tracing::{error, warn}; 15 16#[derive(Serialize)] 17#[serde(rename_all = "camelCase")] 18pub struct AppPassword { 19 pub name: String, 20 pub created_at: String, 21 pub privileged: bool, 22 #[serde(skip_serializing_if = "Option::is_none")] 23 pub scopes: Option<String>, 24 #[serde(skip_serializing_if = "Option::is_none")] 25 pub created_by_controller: Option<String>, 26} 27 28#[derive(Serialize)] 29pub struct ListAppPasswordsOutput { 30 pub passwords: Vec<AppPassword>, 31} 32 33pub async fn list_app_passwords( 34 State(state): State<AppState>, 35 BearerAuth(auth_user): BearerAuth, 36) -> Response { 37 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 38 Ok(id) => id, 39 Err(e) => return ApiError::from(e).into_response(), 40 }; 41 match sqlx::query!( 42 "SELECT name, created_at, privileged, scopes, created_by_controller_did FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", 43 user_id 44 ) 45 .fetch_all(&state.db) 46 .await 47 { 48 Ok(rows) => { 49 let passwords: Vec<AppPassword> = rows 50 .iter() 51 .map(|row| AppPassword { 52 name: row.name.clone(), 53 created_at: row.created_at.to_rfc3339(), 54 privileged: row.privileged, 55 scopes: row.scopes.clone(), 56 created_by_controller: row.created_by_controller_did.clone(), 57 }) 58 .collect(); 59 Json(ListAppPasswordsOutput { passwords }).into_response() 60 } 61 Err(e) => { 62 error!("DB error listing app passwords: {:?}", e); 63 ApiError::InternalError.into_response() 64 } 65 } 66} 67 68#[derive(Deserialize)] 69pub struct CreateAppPasswordInput { 70 pub name: String, 71 pub privileged: Option<bool>, 72 pub scopes: Option<String>, 73} 74 75#[derive(Serialize)] 76#[serde(rename_all = "camelCase")] 77pub struct CreateAppPasswordOutput { 78 pub name: String, 79 pub password: String, 80 pub created_at: String, 81 pub privileged: bool, 82 #[serde(skip_serializing_if = "Option::is_none")] 83 pub scopes: Option<String>, 84} 85 86pub async fn create_app_password( 87 State(state): State<AppState>, 88 headers: HeaderMap, 89 BearerAuth(auth_user): BearerAuth, 90 Json(input): Json<CreateAppPasswordInput>, 91) -> Response { 92 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 93 if !state 94 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 95 .await 96 { 97 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 98 return ( 99 axum::http::StatusCode::TOO_MANY_REQUESTS, 100 Json(json!({ 101 "error": "RateLimitExceeded", 102 "message": "Too many requests. Please try again later." 103 })), 104 ) 105 .into_response(); 106 } 107 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 108 Ok(id) => id, 109 Err(e) => return ApiError::from(e).into_response(), 110 }; 111 let name = input.name.trim(); 112 if name.is_empty() { 113 return ApiError::InvalidRequest("name is required".into()).into_response(); 114 } 115 let existing = sqlx::query!( 116 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", 117 user_id, 118 name 119 ) 120 .fetch_optional(&state.db) 121 .await; 122 if let Ok(Some(_)) = existing { 123 return ApiError::DuplicateAppPassword.into_response(); 124 } 125 126 let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { 127 let grant = delegation::get_delegation(&state.db, &auth_user.did, controller) 128 .await 129 .ok() 130 .flatten(); 131 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default(); 132 133 let requested = input.scopes.as_deref().unwrap_or("atproto"); 134 let intersected = delegation::intersect_scopes(requested, &granted_scopes); 135 136 if intersected.is_empty() && !granted_scopes.is_empty() { 137 return ApiError::InsufficientScope.into_response(); 138 } 139 140 let scope_result = if intersected.is_empty() { 141 None 142 } else { 143 Some(intersected) 144 }; 145 (scope_result, Some(controller.clone())) 146 } else { 147 (input.scopes.clone(), None) 148 }; 149 150 let password: String = (0..4) 151 .map(|_| { 152 use rand::Rng; 153 let mut rng = rand::thread_rng(); 154 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 155 (0..4) 156 .map(|_| chars[rng.gen_range(0..chars.len())]) 157 .collect::<String>() 158 }) 159 .collect::<Vec<String>>() 160 .join("-"); 161 let password_clone = password.clone(); 162 let password_hash = match tokio::task::spawn_blocking(move || { 163 bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST) 164 }) 165 .await 166 { 167 Ok(Ok(h)) => h, 168 Ok(Err(e)) => { 169 error!("Failed to hash password: {:?}", e); 170 return ApiError::InternalError.into_response(); 171 } 172 Err(e) => { 173 error!("Failed to spawn blocking task: {:?}", e); 174 return ApiError::InternalError.into_response(); 175 } 176 }; 177 let privileged = input.privileged.unwrap_or(false); 178 let created_at = chrono::Utc::now(); 179 match sqlx::query!( 180 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged, scopes, created_by_controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7)", 181 user_id, 182 name, 183 password_hash, 184 created_at, 185 privileged, 186 final_scopes, 187 controller_did 188 ) 189 .execute(&state.db) 190 .await 191 { 192 Ok(_) => { 193 if let Some(ref controller) = controller_did { 194 let _ = delegation::log_delegation_action( 195 &state.db, 196 &auth_user.did, 197 controller, 198 Some(controller), 199 DelegationActionType::AccountAction, 200 Some(json!({ 201 "action": "create_app_password", 202 "name": name, 203 "scopes": final_scopes 204 })), 205 None, 206 None, 207 ) 208 .await; 209 } 210 Json(CreateAppPasswordOutput { 211 name: name.to_string(), 212 password, 213 created_at: created_at.to_rfc3339(), 214 privileged, 215 scopes: final_scopes, 216 }) 217 .into_response() 218 } 219 Err(e) => { 220 error!("DB error creating app password: {:?}", e); 221 ApiError::InternalError.into_response() 222 } 223 } 224} 225 226#[derive(Deserialize)] 227pub struct RevokeAppPasswordInput { 228 pub name: String, 229} 230 231pub async fn revoke_app_password( 232 State(state): State<AppState>, 233 BearerAuth(auth_user): BearerAuth, 234 Json(input): Json<RevokeAppPasswordInput>, 235) -> Response { 236 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 237 Ok(id) => id, 238 Err(e) => return ApiError::from(e).into_response(), 239 }; 240 let name = input.name.trim(); 241 if name.is_empty() { 242 return ApiError::InvalidRequest("name is required".into()).into_response(); 243 } 244 let sessions_to_invalidate = sqlx::query_scalar!( 245 "SELECT access_jti FROM session_tokens WHERE did = $1 AND app_password_name = $2", 246 auth_user.did, 247 name 248 ) 249 .fetch_all(&state.db) 250 .await 251 .unwrap_or_default(); 252 if let Err(e) = sqlx::query!( 253 "DELETE FROM session_tokens WHERE did = $1 AND app_password_name = $2", 254 auth_user.did, 255 name 256 ) 257 .execute(&state.db) 258 .await 259 { 260 error!("DB error revoking sessions for app password: {:?}", e); 261 return ApiError::InternalError.into_response(); 262 } 263 for jti in &sessions_to_invalidate { 264 let cache_key = format!("auth:session:{}:{}", auth_user.did, jti); 265 let _ = state.cache.delete(&cache_key).await; 266 } 267 if let Err(e) = sqlx::query!( 268 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 269 user_id, 270 name 271 ) 272 .execute(&state.db) 273 .await 274 { 275 error!("DB error revoking app password: {:?}", e); 276 return ApiError::InternalError.into_response(); 277 } 278 Json(json!({})).into_response() 279}