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