this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::{Query, State}, 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7}; 8use serde::{Deserialize, Serialize}; 9use serde_json::json; 10use sqlx::Row; 11use tracing::error; 12 13#[derive(Deserialize)] 14pub struct GetAccountInfoParams { 15 pub did: String, 16} 17 18#[derive(Serialize)] 19#[serde(rename_all = "camelCase")] 20pub struct AccountInfo { 21 pub did: String, 22 pub handle: String, 23 pub email: Option<String>, 24 pub indexed_at: String, 25 pub invite_note: Option<String>, 26 pub invites_disabled: bool, 27 pub email_confirmed_at: Option<String>, 28 pub deactivated_at: Option<String>, 29} 30 31#[derive(Serialize)] 32#[serde(rename_all = "camelCase")] 33pub struct GetAccountInfosOutput { 34 pub infos: Vec<AccountInfo>, 35} 36 37pub async fn get_account_info( 38 State(state): State<AppState>, 39 headers: axum::http::HeaderMap, 40 Query(params): Query<GetAccountInfoParams>, 41) -> Response { 42 let auth_header = headers.get("Authorization"); 43 if auth_header.is_none() { 44 return ( 45 StatusCode::UNAUTHORIZED, 46 Json(json!({"error": "AuthenticationRequired"})), 47 ) 48 .into_response(); 49 } 50 51 let did = params.did.trim(); 52 if did.is_empty() { 53 return ( 54 StatusCode::BAD_REQUEST, 55 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 56 ) 57 .into_response(); 58 } 59 60 let result = sqlx::query( 61 r#" 62 SELECT did, handle, email, created_at 63 FROM users 64 WHERE did = $1 65 "#, 66 ) 67 .bind(did) 68 .fetch_optional(&state.db) 69 .await; 70 71 match result { 72 Ok(Some(row)) => { 73 let user_did: String = row.get("did"); 74 let handle: String = row.get("handle"); 75 let email: String = row.get("email"); 76 let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at"); 77 78 ( 79 StatusCode::OK, 80 Json(AccountInfo { 81 did: user_did, 82 handle, 83 email: Some(email), 84 indexed_at: created_at.to_rfc3339(), 85 invite_note: None, 86 invites_disabled: false, 87 email_confirmed_at: None, 88 deactivated_at: None, 89 }), 90 ) 91 .into_response() 92 } 93 Ok(None) => ( 94 StatusCode::NOT_FOUND, 95 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 96 ) 97 .into_response(), 98 Err(e) => { 99 error!("DB error in get_account_info: {:?}", e); 100 ( 101 StatusCode::INTERNAL_SERVER_ERROR, 102 Json(json!({"error": "InternalError"})), 103 ) 104 .into_response() 105 } 106 } 107} 108 109#[derive(Deserialize)] 110pub struct GetAccountInfosParams { 111 pub dids: String, 112} 113 114pub async fn get_account_infos( 115 State(state): State<AppState>, 116 headers: axum::http::HeaderMap, 117 Query(params): Query<GetAccountInfosParams>, 118) -> Response { 119 let auth_header = headers.get("Authorization"); 120 if auth_header.is_none() { 121 return ( 122 StatusCode::UNAUTHORIZED, 123 Json(json!({"error": "AuthenticationRequired"})), 124 ) 125 .into_response(); 126 } 127 128 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect(); 129 if dids.is_empty() { 130 return ( 131 StatusCode::BAD_REQUEST, 132 Json(json!({"error": "InvalidRequest", "message": "dids is required"})), 133 ) 134 .into_response(); 135 } 136 137 let mut infos = Vec::new(); 138 139 for did in dids { 140 if did.is_empty() { 141 continue; 142 } 143 144 let result = sqlx::query( 145 r#" 146 SELECT did, handle, email, created_at 147 FROM users 148 WHERE did = $1 149 "#, 150 ) 151 .bind(did) 152 .fetch_optional(&state.db) 153 .await; 154 155 if let Ok(Some(row)) = result { 156 let user_did: String = row.get("did"); 157 let handle: String = row.get("handle"); 158 let email: String = row.get("email"); 159 let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at"); 160 161 infos.push(AccountInfo { 162 did: user_did, 163 handle, 164 email: Some(email), 165 indexed_at: created_at.to_rfc3339(), 166 invite_note: None, 167 invites_disabled: false, 168 email_confirmed_at: None, 169 deactivated_at: None, 170 }); 171 } 172 } 173 174 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 175} 176 177#[derive(Deserialize)] 178pub struct DeleteAccountInput { 179 pub did: String, 180} 181 182pub async fn delete_account( 183 State(state): State<AppState>, 184 headers: axum::http::HeaderMap, 185 Json(input): Json<DeleteAccountInput>, 186) -> Response { 187 let auth_header = headers.get("Authorization"); 188 if auth_header.is_none() { 189 return ( 190 StatusCode::UNAUTHORIZED, 191 Json(json!({"error": "AuthenticationRequired"})), 192 ) 193 .into_response(); 194 } 195 196 let did = input.did.trim(); 197 if did.is_empty() { 198 return ( 199 StatusCode::BAD_REQUEST, 200 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 201 ) 202 .into_response(); 203 } 204 205 let user = sqlx::query("SELECT id FROM users WHERE did = $1") 206 .bind(did) 207 .fetch_optional(&state.db) 208 .await; 209 210 let user_id: uuid::Uuid = match user { 211 Ok(Some(row)) => row.get("id"), 212 Ok(None) => { 213 return ( 214 StatusCode::NOT_FOUND, 215 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 216 ) 217 .into_response(); 218 } 219 Err(e) => { 220 error!("DB error in delete_account: {:?}", e); 221 return ( 222 StatusCode::INTERNAL_SERVER_ERROR, 223 Json(json!({"error": "InternalError"})), 224 ) 225 .into_response(); 226 } 227 }; 228 229 let _ = sqlx::query("DELETE FROM sessions WHERE did = $1") 230 .bind(did) 231 .execute(&state.db) 232 .await; 233 234 let _ = sqlx::query("DELETE FROM records WHERE repo_id = $1") 235 .bind(user_id) 236 .execute(&state.db) 237 .await; 238 239 let _ = sqlx::query("DELETE FROM repos WHERE user_id = $1") 240 .bind(user_id) 241 .execute(&state.db) 242 .await; 243 244 let _ = sqlx::query("DELETE FROM blobs WHERE created_by_user = $1") 245 .bind(user_id) 246 .execute(&state.db) 247 .await; 248 249 let _ = sqlx::query("DELETE FROM user_keys WHERE user_id = $1") 250 .bind(user_id) 251 .execute(&state.db) 252 .await; 253 254 let result = sqlx::query("DELETE FROM users WHERE id = $1") 255 .bind(user_id) 256 .execute(&state.db) 257 .await; 258 259 match result { 260 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 261 Err(e) => { 262 error!("DB error deleting account: {:?}", e); 263 ( 264 StatusCode::INTERNAL_SERVER_ERROR, 265 Json(json!({"error": "InternalError"})), 266 ) 267 .into_response() 268 } 269 } 270} 271 272#[derive(Deserialize)] 273pub struct UpdateAccountEmailInput { 274 pub account: String, 275 pub email: String, 276} 277 278pub async fn update_account_email( 279 State(state): State<AppState>, 280 headers: axum::http::HeaderMap, 281 Json(input): Json<UpdateAccountEmailInput>, 282) -> Response { 283 let auth_header = headers.get("Authorization"); 284 if auth_header.is_none() { 285 return ( 286 StatusCode::UNAUTHORIZED, 287 Json(json!({"error": "AuthenticationRequired"})), 288 ) 289 .into_response(); 290 } 291 292 let account = input.account.trim(); 293 let email = input.email.trim(); 294 295 if account.is_empty() || email.is_empty() { 296 return ( 297 StatusCode::BAD_REQUEST, 298 Json(json!({"error": "InvalidRequest", "message": "account and email are required"})), 299 ) 300 .into_response(); 301 } 302 303 let result = sqlx::query("UPDATE users SET email = $1 WHERE did = $2") 304 .bind(email) 305 .bind(account) 306 .execute(&state.db) 307 .await; 308 309 match result { 310 Ok(r) => { 311 if r.rows_affected() == 0 { 312 return ( 313 StatusCode::NOT_FOUND, 314 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 315 ) 316 .into_response(); 317 } 318 (StatusCode::OK, Json(json!({}))).into_response() 319 } 320 Err(e) => { 321 error!("DB error updating email: {:?}", e); 322 ( 323 StatusCode::INTERNAL_SERVER_ERROR, 324 Json(json!({"error": "InternalError"})), 325 ) 326 .into_response() 327 } 328 } 329} 330 331#[derive(Deserialize)] 332pub struct UpdateAccountHandleInput { 333 pub did: String, 334 pub handle: String, 335} 336 337pub async fn update_account_handle( 338 State(state): State<AppState>, 339 headers: axum::http::HeaderMap, 340 Json(input): Json<UpdateAccountHandleInput>, 341) -> Response { 342 let auth_header = headers.get("Authorization"); 343 if auth_header.is_none() { 344 return ( 345 StatusCode::UNAUTHORIZED, 346 Json(json!({"error": "AuthenticationRequired"})), 347 ) 348 .into_response(); 349 } 350 351 let did = input.did.trim(); 352 let handle = input.handle.trim(); 353 354 if did.is_empty() || handle.is_empty() { 355 return ( 356 StatusCode::BAD_REQUEST, 357 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})), 358 ) 359 .into_response(); 360 } 361 362 if !handle 363 .chars() 364 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 365 { 366 return ( 367 StatusCode::BAD_REQUEST, 368 Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})), 369 ) 370 .into_response(); 371 } 372 373 let existing = sqlx::query("SELECT id FROM users WHERE handle = $1 AND did != $2") 374 .bind(handle) 375 .bind(did) 376 .fetch_optional(&state.db) 377 .await; 378 379 if let Ok(Some(_)) = existing { 380 return ( 381 StatusCode::BAD_REQUEST, 382 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 383 ) 384 .into_response(); 385 } 386 387 let result = sqlx::query("UPDATE users SET handle = $1 WHERE did = $2") 388 .bind(handle) 389 .bind(did) 390 .execute(&state.db) 391 .await; 392 393 match result { 394 Ok(r) => { 395 if r.rows_affected() == 0 { 396 return ( 397 StatusCode::NOT_FOUND, 398 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 399 ) 400 .into_response(); 401 } 402 (StatusCode::OK, Json(json!({}))).into_response() 403 } 404 Err(e) => { 405 error!("DB error updating handle: {:?}", e); 406 ( 407 StatusCode::INTERNAL_SERVER_ERROR, 408 Json(json!({"error": "InternalError"})), 409 ) 410 .into_response() 411 } 412 } 413} 414 415#[derive(Deserialize)] 416pub struct UpdateAccountPasswordInput { 417 pub did: String, 418 pub password: String, 419} 420 421pub async fn update_account_password( 422 State(state): State<AppState>, 423 headers: axum::http::HeaderMap, 424 Json(input): Json<UpdateAccountPasswordInput>, 425) -> Response { 426 let auth_header = headers.get("Authorization"); 427 if auth_header.is_none() { 428 return ( 429 StatusCode::UNAUTHORIZED, 430 Json(json!({"error": "AuthenticationRequired"})), 431 ) 432 .into_response(); 433 } 434 435 let did = input.did.trim(); 436 let password = input.password.trim(); 437 438 if did.is_empty() || password.is_empty() { 439 return ( 440 StatusCode::BAD_REQUEST, 441 Json(json!({"error": "InvalidRequest", "message": "did and password are required"})), 442 ) 443 .into_response(); 444 } 445 446 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 447 Ok(h) => h, 448 Err(e) => { 449 error!("Failed to hash password: {:?}", e); 450 return ( 451 StatusCode::INTERNAL_SERVER_ERROR, 452 Json(json!({"error": "InternalError"})), 453 ) 454 .into_response(); 455 } 456 }; 457 458 let result = sqlx::query("UPDATE users SET password_hash = $1 WHERE did = $2") 459 .bind(&password_hash) 460 .bind(did) 461 .execute(&state.db) 462 .await; 463 464 match result { 465 Ok(r) => { 466 if r.rows_affected() == 0 { 467 return ( 468 StatusCode::NOT_FOUND, 469 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 470 ) 471 .into_response(); 472 } 473 (StatusCode::OK, Json(json!({}))).into_response() 474 } 475 Err(e) => { 476 error!("DB error updating password: {:?}", e); 477 ( 478 StatusCode::INTERNAL_SERVER_ERROR, 479 Json(json!({"error": "InternalError"})), 480 ) 481 .into_response() 482 } 483 } 484}