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