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 84 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 85 .await 86 { 87 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 88 return ( 89 axum::http::StatusCode::TOO_MANY_REQUESTS, 90 Json(json!({ 91 "error": "RateLimitExceeded", 92 "message": "Too many requests. Please try again later." 93 })), 94 ) 95 .into_response(); 96 } 97 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 98 Ok(id) => id, 99 Err(e) => return ApiError::from(e).into_response(), 100 }; 101 let name = input.name.trim(); 102 if name.is_empty() { 103 return ApiError::InvalidRequest("name is required".into()).into_response(); 104 } 105 let existing = sqlx::query!( 106 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", 107 user_id, 108 name 109 ) 110 .fetch_optional(&state.db) 111 .await; 112 if let Ok(Some(_)) = existing { 113 return ApiError::DuplicateAppPassword.into_response(); 114 } 115 let password: String = (0..4) 116 .map(|_| { 117 use rand::Rng; 118 let mut rng = rand::thread_rng(); 119 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 120 (0..4) 121 .map(|_| chars[rng.gen_range(0..chars.len())]) 122 .collect::<String>() 123 }) 124 .collect::<Vec<String>>() 125 .join("-"); 126 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 127 Ok(h) => h, 128 Err(e) => { 129 error!("Failed to hash password: {:?}", e); 130 return ApiError::InternalError.into_response(); 131 } 132 }; 133 let privileged = input.privileged.unwrap_or(false); 134 let created_at = chrono::Utc::now(); 135 match sqlx::query!( 136 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)", 137 user_id, 138 name, 139 password_hash, 140 created_at, 141 privileged 142 ) 143 .execute(&state.db) 144 .await 145 { 146 Ok(_) => Json(CreateAppPasswordOutput { 147 name: name.to_string(), 148 password, 149 created_at: created_at.to_rfc3339(), 150 privileged, 151 }) 152 .into_response(), 153 Err(e) => { 154 error!("DB error creating app password: {:?}", e); 155 ApiError::InternalError.into_response() 156 } 157 } 158} 159 160#[derive(Deserialize)] 161pub struct RevokeAppPasswordInput { 162 pub name: String, 163} 164 165pub async fn revoke_app_password( 166 State(state): State<AppState>, 167 BearerAuth(auth_user): BearerAuth, 168 Json(input): Json<RevokeAppPasswordInput>, 169) -> Response { 170 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 171 Ok(id) => id, 172 Err(e) => return ApiError::from(e).into_response(), 173 }; 174 let name = input.name.trim(); 175 if name.is_empty() { 176 return ApiError::InvalidRequest("name is required".into()).into_response(); 177 } 178 match sqlx::query!( 179 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 180 user_id, 181 name 182 ) 183 .execute(&state.db) 184 .await 185 { 186 Ok(r) => { 187 if r.rows_affected() == 0 { 188 return ApiError::AppPasswordNotFound.into_response(); 189 } 190 Json(json!({})).into_response() 191 } 192 Err(e) => { 193 error!("DB error revoking app password: {:?}", e); 194 ApiError::InternalError.into_response() 195 } 196 } 197}