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