this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::State, 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7}; 8use serde::{Deserialize, Serialize}; 9use serde_json::json; 10use tracing::error; 11 12#[derive(Serialize)] 13#[serde(rename_all = "camelCase")] 14pub struct AppPassword { 15 pub name: String, 16 pub created_at: String, 17 pub privileged: bool, 18} 19 20#[derive(Serialize)] 21pub struct ListAppPasswordsOutput { 22 pub passwords: Vec<AppPassword>, 23} 24 25pub async fn list_app_passwords( 26 State(state): State<AppState>, 27 headers: axum::http::HeaderMap, 28) -> Response { 29 let token = match crate::auth::extract_bearer_token_from_header( 30 headers.get("Authorization").and_then(|h| h.to_str().ok()) 31 ) { 32 Some(t) => t, 33 None => { 34 return ( 35 StatusCode::UNAUTHORIZED, 36 Json(json!({"error": "AuthenticationRequired"})), 37 ) 38 .into_response(); 39 } 40 }; 41 42 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 43 let did = match auth_result { 44 Ok(user) => user.did, 45 Err(e) => { 46 return ( 47 StatusCode::UNAUTHORIZED, 48 Json(json!({"error": e})), 49 ) 50 .into_response(); 51 } 52 }; 53 54 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 55 .fetch_optional(&state.db) 56 .await 57 { 58 Ok(Some(id)) => id, 59 _ => { 60 return ( 61 StatusCode::INTERNAL_SERVER_ERROR, 62 Json(json!({"error": "InternalError"})), 63 ) 64 .into_response(); 65 } 66 }; 67 68 let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id) 69 .fetch_all(&state.db) 70 .await; 71 72 match result { 73 Ok(rows) => { 74 let passwords: Vec<AppPassword> = rows 75 .iter() 76 .map(|row| { 77 AppPassword { 78 name: row.name.clone(), 79 created_at: row.created_at.to_rfc3339(), 80 privileged: row.privileged, 81 } 82 }) 83 .collect(); 84 85 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response() 86 } 87 Err(e) => { 88 error!("DB error listing app passwords: {:?}", e); 89 ( 90 StatusCode::INTERNAL_SERVER_ERROR, 91 Json(json!({"error": "InternalError"})), 92 ) 93 .into_response() 94 } 95 } 96} 97 98#[derive(Deserialize)] 99pub struct CreateAppPasswordInput { 100 pub name: String, 101 pub privileged: Option<bool>, 102} 103 104#[derive(Serialize)] 105#[serde(rename_all = "camelCase")] 106pub struct CreateAppPasswordOutput { 107 pub name: String, 108 pub password: String, 109 pub created_at: String, 110 pub privileged: bool, 111} 112 113pub async fn create_app_password( 114 State(state): State<AppState>, 115 headers: axum::http::HeaderMap, 116 Json(input): Json<CreateAppPasswordInput>, 117) -> Response { 118 let token = match crate::auth::extract_bearer_token_from_header( 119 headers.get("Authorization").and_then(|h| h.to_str().ok()) 120 ) { 121 Some(t) => t, 122 None => { 123 return ( 124 StatusCode::UNAUTHORIZED, 125 Json(json!({"error": "AuthenticationRequired"})), 126 ) 127 .into_response(); 128 } 129 }; 130 131 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 132 let did = match auth_result { 133 Ok(user) => user.did, 134 Err(e) => { 135 return ( 136 StatusCode::UNAUTHORIZED, 137 Json(json!({"error": e})), 138 ) 139 .into_response(); 140 } 141 }; 142 143 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 144 .fetch_optional(&state.db) 145 .await 146 { 147 Ok(Some(id)) => id, 148 _ => { 149 return ( 150 StatusCode::INTERNAL_SERVER_ERROR, 151 Json(json!({"error": "InternalError"})), 152 ) 153 .into_response(); 154 } 155 }; 156 157 let name = input.name.trim(); 158 if name.is_empty() { 159 return ( 160 StatusCode::BAD_REQUEST, 161 Json(json!({"error": "InvalidRequest", "message": "name is required"})), 162 ) 163 .into_response(); 164 } 165 166 let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 167 .fetch_optional(&state.db) 168 .await; 169 170 if let Ok(Some(_)) = existing { 171 return ( 172 StatusCode::BAD_REQUEST, 173 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})), 174 ) 175 .into_response(); 176 } 177 178 let password: String = (0..4) 179 .map(|_| { 180 use rand::Rng; 181 let mut rng = rand::thread_rng(); 182 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 183 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>() 184 }) 185 .collect::<Vec<String>>() 186 .join("-"); 187 188 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 189 Ok(h) => h, 190 Err(e) => { 191 error!("Failed to hash password: {:?}", e); 192 return ( 193 StatusCode::INTERNAL_SERVER_ERROR, 194 Json(json!({"error": "InternalError"})), 195 ) 196 .into_response(); 197 } 198 }; 199 200 let privileged = input.privileged.unwrap_or(false); 201 let created_at = chrono::Utc::now(); 202 203 let result = sqlx::query!( 204 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)", 205 user_id, 206 name, 207 password_hash, 208 created_at, 209 privileged 210 ) 211 .execute(&state.db) 212 .await; 213 214 match result { 215 Ok(_) => ( 216 StatusCode::OK, 217 Json(CreateAppPasswordOutput { 218 name: name.to_string(), 219 password, 220 created_at: created_at.to_rfc3339(), 221 privileged, 222 }), 223 ) 224 .into_response(), 225 Err(e) => { 226 error!("DB error creating app password: {:?}", e); 227 ( 228 StatusCode::INTERNAL_SERVER_ERROR, 229 Json(json!({"error": "InternalError"})), 230 ) 231 .into_response() 232 } 233 } 234} 235 236#[derive(Deserialize)] 237pub struct RevokeAppPasswordInput { 238 pub name: String, 239} 240 241pub async fn revoke_app_password( 242 State(state): State<AppState>, 243 headers: axum::http::HeaderMap, 244 Json(input): Json<RevokeAppPasswordInput>, 245) -> Response { 246 let token = match crate::auth::extract_bearer_token_from_header( 247 headers.get("Authorization").and_then(|h| h.to_str().ok()) 248 ) { 249 Some(t) => t, 250 None => { 251 return ( 252 StatusCode::UNAUTHORIZED, 253 Json(json!({"error": "AuthenticationRequired"})), 254 ) 255 .into_response(); 256 } 257 }; 258 259 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 260 let did = match auth_result { 261 Ok(user) => user.did, 262 Err(e) => { 263 return ( 264 StatusCode::UNAUTHORIZED, 265 Json(json!({"error": e})), 266 ) 267 .into_response(); 268 } 269 }; 270 271 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 272 .fetch_optional(&state.db) 273 .await 274 { 275 Ok(Some(id)) => id, 276 _ => { 277 return ( 278 StatusCode::INTERNAL_SERVER_ERROR, 279 Json(json!({"error": "InternalError"})), 280 ) 281 .into_response(); 282 } 283 }; 284 285 let name = input.name.trim(); 286 if name.is_empty() { 287 return ( 288 StatusCode::BAD_REQUEST, 289 Json(json!({"error": "InvalidRequest", "message": "name is required"})), 290 ) 291 .into_response(); 292 } 293 294 let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 295 .execute(&state.db) 296 .await; 297 298 match result { 299 Ok(r) => { 300 if r.rows_affected() == 0 { 301 return ( 302 StatusCode::NOT_FOUND, 303 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})), 304 ) 305 .into_response(); 306 } 307 (StatusCode::OK, Json(json!({}))).into_response() 308 } 309 Err(e) => { 310 error!("DB error revoking app password: {:?}", e); 311 ( 312 StatusCode::INTERNAL_SERVER_ERROR, 313 Json(json!({"error": "InternalError"})), 314 ) 315 .into_response() 316 } 317 } 318}