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