this repo has no description
1use crate::api::ApiError; 2use crate::auth::BearerAuth; 3use crate::state::{AppState, RateLimitKind}; 4use crate::util::get_user_id_by_did; 5use axum::{ 6 Json, 7 extract::State, 8 http::HeaderMap, 9 response::{IntoResponse, Response}, 10}; 11use serde::{Deserialize, Serialize}; 12use serde_json::json; 13use tracing::{error, warn}; 14 15#[derive(Serialize)] 16#[serde(rename_all = "camelCase")] 17pub struct AppPassword { 18 pub name: String, 19 pub created_at: String, 20 pub privileged: bool, 21 #[serde(skip_serializing_if = "Option::is_none")] 22 pub scopes: Option<String>, 23} 24 25#[derive(Serialize)] 26pub struct ListAppPasswordsOutput { 27 pub passwords: Vec<AppPassword>, 28} 29 30pub async fn list_app_passwords( 31 State(state): State<AppState>, 32 BearerAuth(auth_user): BearerAuth, 33) -> Response { 34 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 35 Ok(id) => id, 36 Err(e) => return ApiError::from(e).into_response(), 37 }; 38 match sqlx::query!( 39 "SELECT name, created_at, privileged, scopes FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", 40 user_id 41 ) 42 .fetch_all(&state.db) 43 .await 44 { 45 Ok(rows) => { 46 let passwords: Vec<AppPassword> = rows 47 .iter() 48 .map(|row| AppPassword { 49 name: row.name.clone(), 50 created_at: row.created_at.to_rfc3339(), 51 privileged: row.privileged, 52 scopes: row.scopes.clone(), 53 }) 54 .collect(); 55 Json(ListAppPasswordsOutput { passwords }).into_response() 56 } 57 Err(e) => { 58 error!("DB error listing app passwords: {:?}", e); 59 ApiError::InternalError.into_response() 60 } 61 } 62} 63 64#[derive(Deserialize)] 65pub struct CreateAppPasswordInput { 66 pub name: String, 67 pub privileged: Option<bool>, 68 pub scopes: Option<String>, 69} 70 71#[derive(Serialize)] 72#[serde(rename_all = "camelCase")] 73pub struct CreateAppPasswordOutput { 74 pub name: String, 75 pub password: String, 76 pub created_at: String, 77 pub privileged: bool, 78 #[serde(skip_serializing_if = "Option::is_none")] 79 pub scopes: Option<String>, 80} 81 82pub async fn create_app_password( 83 State(state): State<AppState>, 84 headers: HeaderMap, 85 BearerAuth(auth_user): BearerAuth, 86 Json(input): Json<CreateAppPasswordInput>, 87) -> Response { 88 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 89 if !state 90 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 91 .await 92 { 93 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 94 return ( 95 axum::http::StatusCode::TOO_MANY_REQUESTS, 96 Json(json!({ 97 "error": "RateLimitExceeded", 98 "message": "Too many requests. Please try again later." 99 })), 100 ) 101 .into_response(); 102 } 103 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 104 Ok(id) => id, 105 Err(e) => return ApiError::from(e).into_response(), 106 }; 107 let name = input.name.trim(); 108 if name.is_empty() { 109 return ApiError::InvalidRequest("name is required".into()).into_response(); 110 } 111 let existing = sqlx::query!( 112 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", 113 user_id, 114 name 115 ) 116 .fetch_optional(&state.db) 117 .await; 118 if let Ok(Some(_)) = existing { 119 return ApiError::DuplicateAppPassword.into_response(); 120 } 121 let password: String = (0..4) 122 .map(|_| { 123 use rand::Rng; 124 let mut rng = rand::thread_rng(); 125 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 126 (0..4) 127 .map(|_| chars[rng.gen_range(0..chars.len())]) 128 .collect::<String>() 129 }) 130 .collect::<Vec<String>>() 131 .join("-"); 132 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 133 Ok(h) => h, 134 Err(e) => { 135 error!("Failed to hash password: {:?}", e); 136 return ApiError::InternalError.into_response(); 137 } 138 }; 139 let privileged = input.privileged.unwrap_or(false); 140 let scopes = input.scopes.clone(); 141 let created_at = chrono::Utc::now(); 142 match sqlx::query!( 143 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged, scopes) VALUES ($1, $2, $3, $4, $5, $6)", 144 user_id, 145 name, 146 password_hash, 147 created_at, 148 privileged, 149 scopes 150 ) 151 .execute(&state.db) 152 .await 153 { 154 Ok(_) => Json(CreateAppPasswordOutput { 155 name: name.to_string(), 156 password, 157 created_at: created_at.to_rfc3339(), 158 privileged, 159 scopes, 160 }) 161 .into_response(), 162 Err(e) => { 163 error!("DB error creating app password: {:?}", e); 164 ApiError::InternalError.into_response() 165 } 166 } 167} 168 169#[derive(Deserialize)] 170pub struct RevokeAppPasswordInput { 171 pub name: String, 172} 173 174pub async fn revoke_app_password( 175 State(state): State<AppState>, 176 BearerAuth(auth_user): BearerAuth, 177 Json(input): Json<RevokeAppPasswordInput>, 178) -> Response { 179 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 180 Ok(id) => id, 181 Err(e) => return ApiError::from(e).into_response(), 182 }; 183 let name = input.name.trim(); 184 if name.is_empty() { 185 return ApiError::InvalidRequest("name is required".into()).into_response(); 186 } 187 match sqlx::query!( 188 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 189 user_id, 190 name 191 ) 192 .execute(&state.db) 193 .await 194 { 195 Ok(r) => { 196 if r.rows_affected() == 0 { 197 return ApiError::AppPasswordNotFound.into_response(); 198 } 199 Json(json!({})).into_response() 200 } 201 Err(e) => { 202 error!("DB error revoking app password: {:?}", e); 203 ApiError::InternalError.into_response() 204 } 205 } 206}