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_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 162 Ok(h) => h, 163 Err(e) => { 164 error!("Failed to hash password: {:?}", e); 165 return ApiError::InternalError.into_response(); 166 } 167 }; 168 let privileged = input.privileged.unwrap_or(false); 169 let created_at = chrono::Utc::now(); 170 match sqlx::query!( 171 "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)", 172 user_id, 173 name, 174 password_hash, 175 created_at, 176 privileged, 177 final_scopes, 178 controller_did 179 ) 180 .execute(&state.db) 181 .await 182 { 183 Ok(_) => { 184 if let Some(ref controller) = controller_did { 185 let _ = delegation::log_delegation_action( 186 &state.db, 187 &auth_user.did, 188 controller, 189 Some(controller), 190 DelegationActionType::AccountAction, 191 Some(json!({ 192 "action": "create_app_password", 193 "name": name, 194 "scopes": final_scopes 195 })), 196 None, 197 None, 198 ) 199 .await; 200 } 201 Json(CreateAppPasswordOutput { 202 name: name.to_string(), 203 password, 204 created_at: created_at.to_rfc3339(), 205 privileged, 206 scopes: final_scopes, 207 }) 208 .into_response() 209 } 210 Err(e) => { 211 error!("DB error creating app password: {:?}", e); 212 ApiError::InternalError.into_response() 213 } 214 } 215} 216 217#[derive(Deserialize)] 218pub struct RevokeAppPasswordInput { 219 pub name: String, 220} 221 222pub async fn revoke_app_password( 223 State(state): State<AppState>, 224 BearerAuth(auth_user): BearerAuth, 225 Json(input): Json<RevokeAppPasswordInput>, 226) -> Response { 227 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 228 Ok(id) => id, 229 Err(e) => return ApiError::from(e).into_response(), 230 }; 231 let name = input.name.trim(); 232 if name.is_empty() { 233 return ApiError::InvalidRequest("name is required".into()).into_response(); 234 } 235 let sessions_to_invalidate = sqlx::query_scalar!( 236 "SELECT access_jti FROM session_tokens WHERE did = $1 AND app_password_name = $2", 237 auth_user.did, 238 name 239 ) 240 .fetch_all(&state.db) 241 .await 242 .unwrap_or_default(); 243 if let Err(e) = sqlx::query!( 244 "DELETE FROM session_tokens WHERE did = $1 AND app_password_name = $2", 245 auth_user.did, 246 name 247 ) 248 .execute(&state.db) 249 .await 250 { 251 error!("DB error revoking sessions for app password: {:?}", e); 252 return ApiError::InternalError.into_response(); 253 } 254 for jti in &sessions_to_invalidate { 255 let cache_key = format!("auth:session:{}:{}", auth_user.did, jti); 256 let _ = state.cache.delete(&cache_key).await; 257 } 258 if let Err(e) = sqlx::query!( 259 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 260 user_id, 261 name 262 ) 263 .execute(&state.db) 264 .await 265 { 266 error!("DB error revoking app password: {:?}", e); 267 return ApiError::InternalError.into_response(); 268 } 269 Json(json!({})).into_response() 270}