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