Our Personal Data Server from scratch! tranquil.farm
atproto pds rust postgresql fun oauth

refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints #86

merged opened by oyster.cafe targeting main from refactor/api
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhi3qdcwaw22
+397 -407
Diff #0
+3 -3
crates/tranquil-api/src/actor/preferences.rs
··· 69 69 if let Some(age) = personal_details_pref 70 70 .as_ref() 71 71 .and_then(|pref| pref.get("birthDate")) 72 - .and_then(|v| v.as_str()) 72 + .and_then(Value::as_str) 73 73 .and_then(get_age_from_datestring) 74 74 { 75 75 let declared_age_pref = serde_json::json!({ ··· 122 122 if pref_str.len() > MAX_PREFERENCE_SIZE { 123 123 return PrefValidation::TooLarge(pref_str.len()); 124 124 } 125 - let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 125 + let pref_type = match pref.get("$type").and_then(Value::as_str) { 126 126 Some(t) => t, 127 127 None => return PrefValidation::MissingType, 128 128 }; ··· 179 179 .preferences 180 180 .into_iter() 181 181 .filter_map(|pref| { 182 - let pref_type = pref.get("$type").and_then(|t| t.as_str())?; 182 + let pref_type = pref.get("$type").and_then(Value::as_str)?; 183 183 if pref_type == DECLARED_AGE_PREF { 184 184 return None; 185 185 }
+43 -20
crates/tranquil-api/src/age_assurance.rs
··· 1 1 use axum::{ 2 2 Json, 3 3 extract::State, 4 - http::{HeaderMap, Method, StatusCode}, 5 - response::{IntoResponse, Response}, 4 + http::{HeaderMap, Method}, 6 5 }; 7 - use serde_json::json; 6 + use serde::Serialize; 8 7 use tranquil_pds::auth::{ 9 8 AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop, 10 9 }; 11 10 use tranquil_pds::state::AppState; 12 11 13 - pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response { 12 + #[derive(Serialize)] 13 + #[serde(rename_all = "camelCase")] 14 + pub struct AgeAssuranceState { 15 + pub status: &'static str, 16 + pub access: &'static str, 17 + pub last_initiated_at: String, 18 + } 19 + 20 + #[derive(Serialize)] 21 + #[serde(rename_all = "camelCase")] 22 + pub struct AgeAssuranceMetadata { 23 + pub account_created_at: Option<String>, 24 + } 25 + 26 + #[derive(Serialize)] 27 + pub struct GetAgeAssuranceOutput { 28 + pub state: AgeAssuranceState, 29 + pub metadata: AgeAssuranceMetadata, 30 + } 31 + 32 + #[derive(Serialize)] 33 + pub struct AgeAssuranceStatusOutput { 34 + pub status: &'static str, 35 + } 36 + 37 + pub async fn get_state( 38 + State(state): State<AppState>, 39 + headers: HeaderMap, 40 + ) -> Json<GetAgeAssuranceOutput> { 14 41 let created_at = get_account_created_at(&state, &headers).await; 15 42 let now = chrono::Utc::now().to_rfc3339(); 16 43 17 - ( 18 - StatusCode::OK, 19 - Json(json!({ 20 - "state": { 21 - "status": "assured", 22 - "access": "full", 23 - "lastInitiatedAt": now 24 - }, 25 - "metadata": { 26 - "accountCreatedAt": created_at 27 - } 28 - })), 29 - ) 30 - .into_response() 44 + Json(GetAgeAssuranceOutput { 45 + state: AgeAssuranceState { 46 + status: "assured", 47 + access: "full", 48 + last_initiated_at: now, 49 + }, 50 + metadata: AgeAssuranceMetadata { 51 + account_created_at: created_at, 52 + }, 53 + }) 31 54 } 32 55 33 - pub async fn get_age_assurance_state() -> Response { 34 - (StatusCode::OK, Json(json!({"status": "assured"}))).into_response() 56 + pub async fn get_age_assurance_state() -> Json<AgeAssuranceStatusOutput> { 57 + Json(AgeAssuranceStatusOutput { status: "assured" }) 35 58 } 36 59 37 60 async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
+75 -135
crates/tranquil-api/src/delegation.rs
··· 2 2 use axum::{ 3 3 Json, 4 4 extract::{Query, State}, 5 - http::StatusCode, 6 - response::{IntoResponse, Response}, 7 5 }; 8 6 use serde::{Deserialize, Serialize}; 9 7 use serde_json::json; 10 8 use tracing::{error, info, warn}; 11 9 use tranquil_pds::api::error::ApiError; 10 + use tranquil_pds::api::{ 11 + AccountsOutput, AuditLogOutput, ControllersOutput, PresetsOutput, SuccessResponse, 12 + }; 12 13 use tranquil_pds::auth::{Active, Auth}; 13 14 use tranquil_pds::delegation::{ 14 15 DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers, ··· 21 22 pub async fn list_controllers( 22 23 State(state): State<AppState>, 23 24 auth: Auth<Active>, 24 - ) -> Result<Response, ApiError> { 25 - let controllers = match state 25 + ) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> { 26 + let controllers = state 26 27 .delegation_repo 27 28 .get_delegations_for_account(&auth.did) 28 29 .await 29 - { 30 - Ok(c) => c, 31 - Err(e) => { 30 + .map_err(|e| { 32 31 tracing::error!("Failed to list controllers: {:?}", e); 33 - return Ok( 34 - ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), 35 - ); 36 - } 37 - }; 32 + ApiError::InternalError(Some("Failed to list controllers".into())) 33 + })?; 38 34 39 35 let resolve_futures = controllers.into_iter().map(|mut c| { 40 36 let did_resolver = state.did_resolver.clone(); ··· 44 40 .resolve_did_document(c.did.as_str()) 45 41 .await 46 42 .and_then(|doc| tranquil_types::did_doc::extract_handle(&doc)) 47 - .map(|h| h.into()); 43 + .map(Into::into); 48 44 } 49 45 c 50 46 } ··· 52 48 53 49 let controllers = futures::future::join_all(resolve_futures).await; 54 50 55 - Ok(Json(serde_json::json!({ "controllers": controllers })).into_response()) 51 + Ok(Json(ControllersOutput { controllers })) 56 52 } 57 53 58 54 #[derive(Debug, Deserialize)] ··· 65 61 State(state): State<AppState>, 66 62 auth: Auth<Active>, 67 63 Json(input): Json<AddControllerInput>, 68 - ) -> Result<Response, ApiError> { 64 + ) -> Result<Json<SuccessResponse>, ApiError> { 69 65 let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did) 70 66 .await 71 67 .ok_or(ApiError::ControllerNotFound)?; ··· 74 70 && let Some(ref pds_url) = resolved.pds_url 75 71 { 76 72 if !pds_url.starts_with("https://") { 77 - return Ok( 78 - ApiError::InvalidDelegation("Controller PDS must use HTTPS".into()).into_response(), 79 - ); 73 + return Err(ApiError::InvalidDelegation( 74 + "Controller PDS must use HTTPS".into(), 75 + )); 80 76 } 81 77 match state 82 78 .cross_pds_oauth ··· 84 80 .await 85 81 { 86 82 Some(true) => { 87 - return Ok(ApiError::InvalidDelegation( 83 + return Err(ApiError::InvalidDelegation( 88 84 "Cannot add a delegated account from another PDS as a controller".into(), 89 - ) 90 - .into_response()); 85 + )); 91 86 } 92 87 Some(false) => {} 93 88 None => { ··· 100 95 } 101 96 } 102 97 103 - let can_add = match verify_can_add_controllers(&state, &auth).await { 104 - Ok(proof) => proof, 105 - Err(response) => return Ok(response), 106 - }; 98 + let can_add = verify_can_add_controllers(&state, &auth).await?; 107 99 108 100 if resolved.is_local 109 101 && state ··· 112 104 .await 113 105 .unwrap_or(false) 114 106 { 115 - return Ok(ApiError::InvalidDelegation( 107 + return Err(ApiError::InvalidDelegation( 116 108 "Cannot add a controlled account as a controller".into(), 117 - ) 118 - .into_response()); 109 + )); 119 110 } 120 111 121 112 match state ··· 136 127 can_add.did(), 137 128 Some(&input.controller_did), 138 129 DelegationActionType::GrantCreated, 139 - Some(serde_json::json!({ 130 + Some(json!({ 140 131 "granted_scopes": input.granted_scopes.as_str(), 141 132 "is_local": resolved.is_local 142 133 })), ··· 145 136 ) 146 137 .await; 147 138 148 - Ok(( 149 - StatusCode::OK, 150 - Json(serde_json::json!({ 151 - "success": true 152 - })), 153 - ) 154 - .into_response()) 139 + Ok(Json(SuccessResponse { success: true })) 155 140 } 156 141 Err(e) => { 157 142 tracing::error!("Failed to add controller: {:?}", e); 158 - Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) 143 + Err(ApiError::InternalError(Some( 144 + "Failed to add controller".into(), 145 + ))) 159 146 } 160 147 } 161 148 } ··· 169 156 State(state): State<AppState>, 170 157 auth: Auth<Active>, 171 158 Json(input): Json<RemoveControllerInput>, 172 - ) -> Result<Response, ApiError> { 159 + ) -> Result<Json<SuccessResponse>, ApiError> { 173 160 match state 174 161 .delegation_repo 175 162 .revoke_delegation(&auth.did, &input.controller_did, &auth.did) ··· 197 184 &auth.did, 198 185 Some(&input.controller_did), 199 186 DelegationActionType::GrantRevoked, 200 - Some(serde_json::json!({ 187 + Some(json!({ 201 188 "revoked_app_passwords": revoked_app_passwords, 202 189 "revoked_oauth_tokens": revoked_oauth_tokens 203 190 })), ··· 206 193 ) 207 194 .await; 208 195 209 - Ok(( 210 - StatusCode::OK, 211 - Json(serde_json::json!({ 212 - "success": true 213 - })), 214 - ) 215 - .into_response()) 196 + Ok(Json(SuccessResponse { success: true })) 216 197 } 217 - Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 198 + Ok(false) => Err(ApiError::DelegationNotFound), 218 199 Err(e) => { 219 200 tracing::error!("Failed to remove controller: {:?}", e); 220 - Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) 201 + Err(ApiError::InternalError(Some( 202 + "Failed to remove controller".into(), 203 + ))) 221 204 } 222 205 } 223 206 } ··· 232 215 State(state): State<AppState>, 233 216 auth: Auth<Active>, 234 217 Json(input): Json<UpdateControllerScopesInput>, 235 - ) -> Result<Response, ApiError> { 218 + ) -> Result<Json<SuccessResponse>, ApiError> { 236 219 match state 237 220 .delegation_repo 238 221 .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) ··· 246 229 &auth.did, 247 230 Some(&input.controller_did), 248 231 DelegationActionType::ScopesModified, 249 - Some(serde_json::json!({ 232 + Some(json!({ 250 233 "new_scopes": input.granted_scopes.as_str() 251 234 })), 252 235 None, ··· 254 237 ) 255 238 .await; 256 239 257 - Ok(( 258 - StatusCode::OK, 259 - Json(serde_json::json!({ 260 - "success": true 261 - })), 262 - ) 263 - .into_response()) 240 + Ok(Json(SuccessResponse { success: true })) 264 241 } 265 - Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 242 + Ok(false) => Err(ApiError::DelegationNotFound), 266 243 Err(e) => { 267 244 tracing::error!("Failed to update controller scopes: {:?}", e); 268 - Ok( 269 - ApiError::InternalError(Some("Failed to update controller scopes".into())) 270 - .into_response(), 271 - ) 245 + Err(ApiError::InternalError(Some( 246 + "Failed to update controller scopes".into(), 247 + ))) 272 248 } 273 249 } 274 250 } ··· 276 252 pub async fn list_controlled_accounts( 277 253 State(state): State<AppState>, 278 254 auth: Auth<Active>, 279 - ) -> Result<Response, ApiError> { 280 - let accounts = match state 255 + ) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> { 256 + let accounts = state 281 257 .delegation_repo 282 258 .get_accounts_controlled_by(&auth.did) 283 259 .await 284 - { 285 - Ok(a) => a, 286 - Err(e) => { 260 + .map_err(|e| { 287 261 tracing::error!("Failed to list controlled accounts: {:?}", e); 288 - return Ok( 289 - ApiError::InternalError(Some("Failed to list controlled accounts".into())) 290 - .into_response(), 291 - ); 292 - } 293 - }; 262 + ApiError::InternalError(Some("Failed to list controlled accounts".into())) 263 + })?; 294 264 295 - Ok(Json(serde_json::json!({ "accounts": accounts })).into_response()) 265 + Ok(Json(AccountsOutput { accounts })) 296 266 } 297 267 298 268 #[derive(Debug, Deserialize)] ··· 311 281 State(state): State<AppState>, 312 282 auth: Auth<Active>, 313 283 Query(params): Query<AuditLogParams>, 314 - ) -> Result<Response, ApiError> { 284 + ) -> Result<Json<AuditLogOutput<Vec<tranquil_db_traits::AuditLogEntry>>>, ApiError> { 315 285 let limit = params.limit.clamp(1, 100); 316 286 let offset = params.offset.max(0); 317 287 318 - let entries = match state 288 + let entries = state 319 289 .delegation_repo 320 290 .get_audit_log_for_account(&auth.did, limit, offset) 321 291 .await 322 - { 323 - Ok(e) => e, 324 - Err(e) => { 292 + .map_err(|e| { 325 293 tracing::error!("Failed to get audit log: {:?}", e); 326 - return Ok( 327 - ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), 328 - ); 329 - } 330 - }; 294 + ApiError::InternalError(Some("Failed to get audit log".into())) 295 + })?; 331 296 332 297 let total = state 333 298 .delegation_repo ··· 335 300 .await 336 301 .unwrap_or_default(); 337 302 338 - Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response()) 303 + Ok(Json(AuditLogOutput { entries, total })) 339 304 } 340 305 341 - pub async fn get_scope_presets() -> Response { 342 - Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response() 306 + pub async fn get_scope_presets() 307 + -> Json<PresetsOutput<&'static [tranquil_pds::delegation::ScopePreset]>> { 308 + Json(PresetsOutput { 309 + presets: SCOPE_PRESETS, 310 + }) 343 311 } 344 312 345 313 #[derive(Debug, Deserialize)] ··· 353 321 354 322 #[derive(Debug, Serialize)] 355 323 #[serde(rename_all = "camelCase")] 356 - pub struct CreateDelegatedAccountResponse { 324 + pub struct CreateDelegatedAccountOutput { 357 325 pub did: Did, 358 326 pub handle: Handle, 359 327 } ··· 363 331 _rate_limit: RateLimited<AccountCreationLimit>, 364 332 auth: Auth<Active>, 365 333 Json(input): Json<CreateDelegatedAccountInput>, 366 - ) -> Result<Response, ApiError> { 367 - let can_control = match verify_can_control_accounts(&state, &auth).await { 368 - Ok(proof) => proof, 369 - Err(response) => return Ok(response), 370 - }; 334 + ) -> Result<Json<CreateDelegatedAccountOutput>, ApiError> { 335 + let can_control = verify_can_control_accounts(&state, &auth).await?; 371 336 372 - let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) { 373 - Ok(h) => h, 374 - Err(e) => { 375 - return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); 376 - } 377 - }; 337 + let handle = tranquil_pds::api::validation::resolve_handle_input(&input.handle) 338 + .map_err(|e| ApiError::InvalidRequest(e.to_string()))?; 378 339 379 340 let email = input 380 341 .email ··· 384 345 if let Some(ref email) = email 385 346 && !tranquil_pds::api::validation::is_valid_email(email) 386 347 { 387 - return Ok(ApiError::InvalidEmail.into_response()); 348 + return Err(ApiError::InvalidEmail); 388 349 } 389 350 390 351 let validated_invite_code = if let Some(ref code) = input.invite_code { 391 352 match state.infra_repo.validate_invite_code(code).await { 392 353 Ok(validated) => Some(validated), 393 - Err(_) => return Ok(ApiError::InvalidInviteCode.into_response()), 354 + Err(_) => return Err(ApiError::InvalidInviteCode), 394 355 } 395 356 } else { 396 357 let invite_required = tranquil_config::get().server.invite_code_required; 397 358 if invite_required { 398 - return Ok(ApiError::InviteCodeRequired.into_response()); 359 + return Err(ApiError::InviteCodeRequired); 399 360 } 400 361 None 401 362 }; ··· 409 370 info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account"); 410 371 411 372 let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?; 373 + let repo_for_seq = repo.clone(); 412 374 413 375 let create_input = tranquil_db_traits::CreateDelegatedAccountInput { 414 376 handle: handle.clone(), ··· 431 393 { 432 394 Ok(id) => id, 433 395 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 434 - return Ok(ApiError::HandleNotAvailable(None).into_response()); 396 + return Err(ApiError::HandleNotAvailable(None)); 435 397 } 436 398 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 437 - return Ok(ApiError::EmailTaken.into_response()); 399 + return Err(ApiError::EmailTaken); 438 400 } 439 401 Err(e) => { 440 402 error!("Error creating delegated account: {:?}", e); 441 - return Ok(ApiError::InternalError(None).into_response()); 403 + return Err(ApiError::InternalError(None)); 442 404 } 443 405 }; 444 406 ··· 451 413 warn!("Failed to record invite code use for {}: {:?}", did, e); 452 414 } 453 415 454 - if let Err(e) = 455 - tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await 456 - { 457 - warn!("Failed to sequence identity event for {}: {}", did, e); 458 - } 459 - if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( 416 + crate::identity::provision::sequence_new_account( 460 417 &state, 461 418 &did, 462 - tranquil_db_traits::AccountStatus::Active, 419 + &handle, 420 + &repo_for_seq, 421 + handle.as_str(), 463 422 ) 464 - .await 465 - { 466 - warn!("Failed to sequence account event for {}: {}", did, e); 467 - } 468 - 469 - let profile_record = json!({ 470 - "$type": "app.bsky.actor.profile", 471 - "displayName": handle 472 - }); 473 - if let Err(e) = tranquil_pds::repo_ops::create_record_internal( 474 - &state, 475 - &did, 476 - &tranquil_pds::types::PROFILE_COLLECTION, 477 - &tranquil_pds::types::PROFILE_RKEY, 478 - &profile_record, 479 - ) 480 - .await 481 - { 482 - warn!("Failed to create default profile for {}: {}", did, e); 483 - } 423 + .await; 484 424 485 425 let _ = state 486 426 .delegation_repo ··· 500 440 501 441 info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created"); 502 442 503 - Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 443 + Ok(Json(CreateDelegatedAccountOutput { did, handle })) 504 444 } 505 445 506 446 #[derive(Debug, Deserialize)] ··· 511 451 pub async fn resolve_controller( 512 452 State(state): State<AppState>, 513 453 Query(params): Query<ResolveControllerParams>, 514 - ) -> Result<Response, ApiError> { 454 + ) -> Result<Json<tranquil_pds::delegation::ResolvedIdentity>, ApiError> { 515 455 let identifier = params.identifier.trim().trim_start_matches('@'); 516 456 517 457 let did: Did = if identifier.starts_with("did:") { ··· 538 478 .await 539 479 .ok_or(ApiError::ControllerNotFound)?; 540 480 541 - Ok(Json(resolved).into_response()) 481 + Ok(Json(resolved)) 542 482 }
+145 -124
crates/tranquil-api/src/notification_prefs.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::State, 4 - response::{IntoResponse, Response}, 5 - }; 1 + use axum::{Json, extract::State}; 6 2 use serde::{Deserialize, Serialize}; 7 3 use serde_json::json; 8 4 use tracing::info; 9 5 use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType}; 10 - use tranquil_pds::api::error::ApiError; 6 + use tranquil_pds::api::error::{ApiError, DbResultExt}; 11 7 use tranquil_pds::auth::{Active, Auth}; 12 8 use tranquil_pds::state::AppState; 13 9 use tranquil_types::Did; 14 10 15 11 #[derive(Serialize)] 16 12 #[serde(rename_all = "camelCase")] 17 - pub struct NotificationPrefsResponse { 13 + pub struct NotificationPrefsOutput { 18 14 pub preferred_channel: CommsChannel, 19 15 pub email: String, 20 16 pub discord_username: Option<String>, ··· 28 24 pub async fn get_notification_prefs( 29 25 State(state): State<AppState>, 30 26 auth: Auth<Active>, 31 - ) -> Result<Response, ApiError> { 27 + ) -> Result<Json<NotificationPrefsOutput>, ApiError> { 32 28 let prefs = state 33 29 .user_repo 34 30 .get_notification_prefs(&auth.did) 35 31 .await 36 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 32 + .log_db_err("get notification prefs")? 37 33 .ok_or(ApiError::AccountNotFound)?; 38 - Ok(Json(NotificationPrefsResponse { 34 + Ok(Json(NotificationPrefsOutput { 39 35 preferred_channel: prefs.preferred_channel, 40 36 email: prefs.email, 41 37 discord_username: prefs.discord_username, ··· 44 40 telegram_verified: prefs.telegram_verified, 45 41 signal_username: prefs.signal_username, 46 42 signal_verified: prefs.signal_verified, 47 - }) 48 - .into_response()) 43 + })) 49 44 } 50 45 51 46 #[derive(Serialize)] ··· 61 56 62 57 #[derive(Serialize)] 63 58 #[serde(rename_all = "camelCase")] 64 - pub struct GetNotificationHistoryResponse { 59 + pub struct GetNotificationHistoryOutput { 65 60 pub notifications: Vec<NotificationHistoryEntry>, 66 61 } 67 62 68 63 pub async fn get_notification_history( 69 64 State(state): State<AppState>, 70 65 auth: Auth<Active>, 71 - ) -> Result<Response, ApiError> { 66 + ) -> Result<Json<GetNotificationHistoryOutput>, ApiError> { 72 67 let user_id = state 73 68 .user_repo 74 69 .get_id_by_did(&auth.did) 75 70 .await 76 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 71 + .log_db_err("get user id by did")? 77 72 .ok_or(ApiError::AccountNotFound)?; 78 73 79 74 let rows = state 80 75 .infra_repo 81 76 .get_notification_history(user_id, 50) 82 77 .await 83 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 78 + .log_db_err("get notification history")?; 84 79 85 80 let sensitive_types = [ 86 81 CommsType::EmailVerification, ··· 112 107 }) 113 108 .collect(); 114 109 115 - Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) 110 + Ok(Json(GetNotificationHistoryOutput { notifications })) 116 111 } 117 112 118 113 #[derive(Deserialize)] ··· 127 122 128 123 #[derive(Serialize)] 129 124 #[serde(rename_all = "camelCase")] 130 - pub struct UpdateNotificationPrefsResponse { 125 + pub struct UpdateNotificationPrefsOutput { 131 126 pub success: bool, 132 127 #[serde(skip_serializing_if = "Vec::is_empty")] 133 128 pub verification_required: Vec<CommsChannel>, ··· 159 154 hostname, 160 155 ) 161 156 .await 162 - .map_err(|e| { 163 - ApiError::InternalError(Some(format!( 164 - "Failed to enqueue email notification: {}", 165 - e 166 - ))) 167 - })?; 157 + .log_db_err("enqueue email verification")?; 168 158 } 169 159 _ => { 170 160 let hostname = &tranquil_config::get().server.hostname; ··· 216 206 Some(json!({"code": formatted_token})), 217 207 ) 218 208 .await 219 - .map_err(|e| { 220 - ApiError::InternalError(Some(format!("Failed to enqueue notification: {}", e))) 221 - })?; 209 + .log_db_err("enqueue channel verification")?; 222 210 } 223 211 } 224 212 225 213 Ok(token) 226 214 } 227 215 216 + async fn process_messaging_channel_update( 217 + state: &AppState, 218 + user_id: uuid::Uuid, 219 + did: &Did, 220 + channel: CommsChannel, 221 + raw_value: &str, 222 + effective_channel: CommsChannel, 223 + verification_required: &mut Vec<CommsChannel>, 224 + ) -> Result<(), ApiError> { 225 + let clean = match channel { 226 + CommsChannel::Discord => raw_value.trim().to_lowercase(), 227 + CommsChannel::Telegram => raw_value.trim_start_matches('@').to_string(), 228 + CommsChannel::Signal => raw_value.trim().trim_start_matches('@').to_lowercase(), 229 + CommsChannel::Email => raw_value.trim().to_lowercase(), 230 + }; 231 + 232 + if clean.is_empty() { 233 + if effective_channel == channel { 234 + return Err(ApiError::InvalidRequest(format!( 235 + "Cannot remove {:?} while it is the preferred notification channel", 236 + channel 237 + ))); 238 + } 239 + match channel { 240 + CommsChannel::Discord => state 241 + .user_repo 242 + .clear_discord(user_id) 243 + .await 244 + .log_db_err("clear discord")?, 245 + CommsChannel::Telegram => state 246 + .user_repo 247 + .clear_telegram(user_id) 248 + .await 249 + .log_db_err("clear telegram")?, 250 + CommsChannel::Signal => state 251 + .user_repo 252 + .clear_signal(user_id) 253 + .await 254 + .log_db_err("clear signal")?, 255 + CommsChannel::Email => {} 256 + }; 257 + info!(did = %did, channel = ?channel, "Cleared channel"); 258 + return Ok(()); 259 + } 260 + 261 + let valid = match channel { 262 + CommsChannel::Discord => tranquil_pds::api::validation::is_valid_discord_username(&clean), 263 + CommsChannel::Telegram => tranquil_pds::api::validation::is_valid_telegram_username(&clean), 264 + CommsChannel::Signal => tranquil_pds::comms::is_valid_signal_username(&clean), 265 + CommsChannel::Email => tranquil_pds::api::validation::is_valid_email(&clean), 266 + }; 267 + if !valid { 268 + return Err(match channel { 269 + CommsChannel::Discord => ApiError::InvalidRequest( 270 + "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(), 271 + ), 272 + CommsChannel::Telegram => ApiError::InvalidRequest( 273 + "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(), 274 + ), 275 + CommsChannel::Signal => ApiError::InvalidRequest( 276 + "Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)".into(), 277 + ), 278 + CommsChannel::Email => ApiError::InvalidEmail, 279 + }); 280 + } 281 + 282 + match channel { 283 + CommsChannel::Discord => state 284 + .user_repo 285 + .set_unverified_discord(user_id, &clean) 286 + .await 287 + .log_db_err("set unverified discord")?, 288 + CommsChannel::Telegram => state 289 + .user_repo 290 + .set_unverified_telegram(user_id, &clean) 291 + .await 292 + .log_db_err("set unverified telegram")?, 293 + CommsChannel::Signal => state 294 + .user_repo 295 + .set_unverified_signal(user_id, &clean) 296 + .await 297 + .log_db_err("set unverified signal")?, 298 + CommsChannel::Email => {} 299 + }; 300 + 301 + if matches!(channel, CommsChannel::Signal) { 302 + request_channel_verification(state, user_id, did, channel, &clean, None).await?; 303 + } 304 + 305 + verification_required.push(channel); 306 + info!(did = %did, channel = ?channel, value = %clean, "Stored unverified channel username"); 307 + Ok(()) 308 + } 309 + 228 310 pub async fn update_notification_prefs( 229 311 State(state): State<AppState>, 230 312 auth: Auth<Active>, 231 313 Json(input): Json<UpdateNotificationPrefsInput>, 232 - ) -> Result<Response, ApiError> { 314 + ) -> Result<Json<UpdateNotificationPrefsOutput>, ApiError> { 233 315 let user_row = state 234 316 .user_repo 235 317 .get_id_handle_email_by_did(&auth.did) 236 318 .await 237 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 319 + .log_db_err("get user by did")? 238 320 .ok_or(ApiError::AccountNotFound)?; 239 321 240 322 let user_id = user_row.id; ··· 245 327 .user_repo 246 328 .get_notification_prefs(&auth.did) 247 329 .await 248 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 330 + .log_db_err("get notification prefs for update")? 249 331 .ok_or(ApiError::AccountNotFound)?; 250 332 251 333 let effective_channel = input ··· 268 350 .user_repo 269 351 .update_preferred_comms_channel(&auth.did, effective_channel) 270 352 .await 271 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 353 + .log_db_err("update preferred channel")?; 272 354 info!(did = %auth.did, channel = ?effective_channel, "Updated preferred notification channel"); 273 355 } 274 356 ··· 298 380 } 299 381 300 382 if let Some(ref discord_username) = input.discord_username { 301 - let discord_clean = discord_username.trim().to_lowercase(); 302 - if discord_clean.is_empty() { 303 - if effective_channel == CommsChannel::Discord { 304 - return Err(ApiError::InvalidRequest( 305 - "Cannot remove Discord while it is the preferred notification channel".into(), 306 - )); 307 - } 308 - state 309 - .user_repo 310 - .clear_discord(user_id) 311 - .await 312 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 313 - info!(did = %auth.did, "Cleared Discord"); 314 - } else if !tranquil_pds::api::validation::is_valid_discord_username(&discord_clean) { 315 - return Err(ApiError::InvalidRequest( 316 - "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)" 317 - .into(), 318 - )); 319 - } else { 320 - state 321 - .user_repo 322 - .set_unverified_discord(user_id, &discord_clean) 323 - .await 324 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 325 - verification_required.push(CommsChannel::Discord); 326 - info!(did = %auth.did, discord_username = %discord_clean, "Stored unverified Discord username"); 327 - } 383 + process_messaging_channel_update( 384 + &state, 385 + user_id, 386 + &auth.did, 387 + CommsChannel::Discord, 388 + discord_username, 389 + effective_channel, 390 + &mut verification_required, 391 + ) 392 + .await?; 328 393 } 329 394 330 395 if let Some(ref telegram) = input.telegram_username { 331 - let telegram_clean = telegram.trim_start_matches('@'); 332 - if telegram_clean.is_empty() { 333 - if effective_channel == CommsChannel::Telegram { 334 - return Err(ApiError::InvalidRequest( 335 - "Cannot remove Telegram while it is the preferred notification channel".into(), 336 - )); 337 - } 338 - state 339 - .user_repo 340 - .clear_telegram(user_id) 341 - .await 342 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 343 - info!(did = %auth.did, "Cleared Telegram username"); 344 - } else if !tranquil_pds::api::validation::is_valid_telegram_username(telegram_clean) { 345 - return Err(ApiError::InvalidRequest( 346 - "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore" 347 - .into(), 348 - )); 349 - } else { 350 - state 351 - .user_repo 352 - .set_unverified_telegram(user_id, telegram_clean) 353 - .await 354 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 355 - verification_required.push(CommsChannel::Telegram); 356 - info!(did = %auth.did, telegram_username = %telegram_clean, "Stored unverified Telegram username"); 357 - } 396 + process_messaging_channel_update( 397 + &state, 398 + user_id, 399 + &auth.did, 400 + CommsChannel::Telegram, 401 + telegram, 402 + effective_channel, 403 + &mut verification_required, 404 + ) 405 + .await?; 358 406 } 359 407 360 408 if let Some(ref signal) = input.signal_username { 361 - let signal_clean = signal.trim().trim_start_matches('@').to_lowercase(); 362 - if signal_clean.is_empty() { 363 - if effective_channel == CommsChannel::Signal { 364 - return Err(ApiError::InvalidRequest( 365 - "Cannot remove Signal while it is the preferred notification channel".into(), 366 - )); 367 - } 368 - state 369 - .user_repo 370 - .clear_signal(user_id) 371 - .await 372 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 373 - info!(did = %auth.did, "Cleared Signal username"); 374 - } else if !tranquil_pds::comms::is_valid_signal_username(&signal_clean) { 375 - return Err(ApiError::InvalidRequest( 376 - "Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)" 377 - .into(), 378 - )); 379 - } else { 380 - state 381 - .user_repo 382 - .set_unverified_signal(user_id, &signal_clean) 383 - .await 384 - .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 385 - request_channel_verification( 386 - &state, 387 - user_id, 388 - &auth.did, 389 - CommsChannel::Signal, 390 - &signal_clean, 391 - None, 392 - ) 393 - .await?; 394 - verification_required.push(CommsChannel::Signal); 395 - info!(did = %auth.did, signal_username = %signal_clean, "Stored unverified Signal username"); 396 - } 409 + process_messaging_channel_update( 410 + &state, 411 + user_id, 412 + &auth.did, 413 + CommsChannel::Signal, 414 + signal, 415 + effective_channel, 416 + &mut verification_required, 417 + ) 418 + .await?; 397 419 } 398 420 399 - Ok(Json(UpdateNotificationPrefsResponse { 421 + Ok(Json(UpdateNotificationPrefsOutput { 400 422 success: true, 401 423 verification_required, 402 - }) 403 - .into_response()) 424 + })) 404 425 }
+57 -79
crates/tranquil-api/src/server/email.rs
··· 12 12 use tracing::{error, info, warn}; 13 13 use tranquil_db_traits::CommsChannel; 14 14 use tranquil_pds::api::error::{ApiError, DbResultExt}; 15 - use tranquil_pds::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 15 + use tranquil_pds::api::{ 16 + EmailUpdateStatusOutput, EmptyResponse, InUseOutput, TokenRequiredResponse, VerifiedResponse, 17 + }; 16 18 use tranquil_pds::auth::{Auth, NotTakendown}; 19 + use tranquil_pds::oauth::scopes::{AccountAction, AccountAttr}; 17 20 use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit}; 18 21 use tranquil_pds::state::AppState; 19 22 ··· 48 51 _rate_limit: RateLimited<EmailUpdateLimit>, 49 52 auth: Auth<NotTakendown>, 50 53 input: Option<Json<RequestEmailUpdateInput>>, 51 - ) -> Result<Response, ApiError> { 52 - if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( 53 - &auth.auth_source, 54 - auth.scope.as_deref(), 55 - tranquil_pds::oauth::scopes::AccountAttr::Email, 56 - tranquil_pds::oauth::scopes::AccountAction::Manage, 57 - ) { 58 - return Ok(e); 59 - } 54 + ) -> Result<Json<TokenRequiredResponse>, ApiError> { 55 + auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?; 60 56 61 57 let user = state 62 58 .user_repo ··· 119 115 } 120 116 121 117 info!("Email update requested for user {}", user.id); 122 - Ok(TokenRequiredResponse::response(token_required).into_response()) 118 + Ok(Json(TokenRequiredResponse { token_required })) 123 119 } 124 120 125 121 #[derive(Deserialize)] ··· 134 130 _rate_limit: RateLimited<EmailUpdateLimit>, 135 131 auth: Auth<NotTakendown>, 136 132 Json(input): Json<ConfirmEmailInput>, 137 - ) -> Result<Response, ApiError> { 138 - if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( 139 - &auth.auth_source, 140 - auth.scope.as_deref(), 141 - tranquil_pds::oauth::scopes::AccountAttr::Email, 142 - tranquil_pds::oauth::scopes::AccountAction::Manage, 143 - ) { 144 - return Ok(e); 145 - } 133 + ) -> Result<Json<EmptyResponse>, ApiError> { 134 + auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?; 146 135 147 136 let did = &auth.did; 148 137 let user = state ··· 163 152 } 164 153 165 154 if user.email_verified { 166 - return Ok(EmptyResponse::ok().into_response()); 155 + return Ok(Json(EmptyResponse {})); 167 156 } 168 157 169 158 let confirmation_code = ··· 196 185 .log_db_err("confirming email")?; 197 186 198 187 info!("Email confirmed for user {}", user.id); 199 - Ok(EmptyResponse::ok().into_response()) 188 + Ok(Json(EmptyResponse {})) 200 189 } 201 190 202 191 #[derive(Deserialize)] ··· 212 201 State(state): State<AppState>, 213 202 auth: Auth<NotTakendown>, 214 203 Json(input): Json<UpdateEmailInput>, 215 - ) -> Result<Response, ApiError> { 216 - if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( 217 - &auth.auth_source, 218 - auth.scope.as_deref(), 219 - tranquil_pds::oauth::scopes::AccountAttr::Email, 220 - tranquil_pds::oauth::scopes::AccountAction::Manage, 221 - ) { 222 - return Ok(e); 223 - } 204 + ) -> Result<Json<EmptyResponse>, ApiError> { 205 + auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?; 224 206 225 207 let did = &auth.did; 226 208 let user = state ··· 279 261 ApiError::InternalError(Some("Failed to update 2FA setting".into())) 280 262 })?; 281 263 } 282 - return Ok(EmptyResponse::ok().into_response()); 264 + return Ok(Json(EmptyResponse {})); 283 265 } 284 266 285 267 if email_verified { ··· 394 376 } 395 377 396 378 info!("Email updated for user {}", user_id); 397 - Ok(EmptyResponse::ok().into_response()) 379 + Ok(Json(EmptyResponse {})) 398 380 } 399 381 400 382 #[derive(Deserialize)] ··· 406 388 State(state): State<AppState>, 407 389 _rate_limit: RateLimited<VerificationCheckLimit>, 408 390 Json(input): Json<CheckEmailVerifiedInput>, 409 - ) -> Response { 410 - match state 391 + ) -> Result<Json<VerifiedResponse>, ApiError> { 392 + let verified = state 411 393 .user_repo 412 394 .check_email_verified_by_identifier(&input.identifier) 413 395 .await 414 - { 415 - Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(), 416 - Ok(None) => ApiError::AccountNotFound.into_response(), 417 - Err(e) => { 396 + .map_err(|e| { 418 397 error!("DB error checking email verified: {:?}", e); 419 - ApiError::InternalError(None).into_response() 420 - } 421 - } 398 + ApiError::InternalError(None) 399 + })? 400 + .ok_or(ApiError::AccountNotFound)?; 401 + 402 + Ok(Json(VerifiedResponse { verified })) 422 403 } 423 404 424 405 #[derive(Deserialize)] ··· 431 412 State(state): State<AppState>, 432 413 _rate_limit: RateLimited<VerificationCheckLimit>, 433 414 Json(input): Json<CheckChannelVerifiedInput>, 434 - ) -> Response { 435 - match state 415 + ) -> Result<Json<VerifiedResponse>, ApiError> { 416 + let verified = state 436 417 .user_repo 437 418 .check_channel_verified_by_did(&input.did, input.channel) 438 419 .await 439 - { 440 - Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(), 441 - Ok(None) => ApiError::AccountNotFound.into_response(), 442 - Err(e) => { 420 + .map_err(|e| { 443 421 error!("DB error checking channel verified: {:?}", e); 444 - ApiError::InternalError(None).into_response() 445 - } 446 - } 422 + ApiError::InternalError(None) 423 + })? 424 + .ok_or(ApiError::AccountNotFound)?; 425 + 426 + Ok(Json(VerifiedResponse { verified })) 447 427 } 448 428 449 429 #[derive(Deserialize)] ··· 545 525 State(state): State<AppState>, 546 526 _rate_limit: RateLimited<VerificationCheckLimit>, 547 527 auth: Auth<NotTakendown>, 548 - ) -> Result<Response, ApiError> { 549 - if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( 550 - &auth.auth_source, 551 - auth.scope.as_deref(), 552 - tranquil_pds::oauth::scopes::AccountAttr::Email, 553 - tranquil_pds::oauth::scopes::AccountAction::Read, 554 - ) { 555 - return Ok(e); 556 - } 528 + ) -> Result<Json<EmailUpdateStatusOutput>, ApiError> { 529 + auth.check_account_scope(AccountAttr::Email, AccountAction::Read)?; 557 530 558 531 let cache_key = email_update_cache_key(&auth.did); 559 532 let pending_json = match state.cache.get(&cache_key).await { 560 533 Some(json) => json, 561 534 None => { 562 - return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 535 + return Ok(Json(EmailUpdateStatusOutput { 536 + pending: false, 537 + authorized: false, 538 + new_email: None, 539 + })); 563 540 } 564 541 }; 565 542 566 543 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 567 544 Ok(p) => p, 568 545 Err(_) => { 569 - return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 546 + return Ok(Json(EmailUpdateStatusOutput { 547 + pending: false, 548 + authorized: false, 549 + new_email: None, 550 + })); 570 551 } 571 552 }; 572 553 573 - Ok(Json(json!({ 574 - "pending": true, 575 - "authorized": pending.authorized, 576 - "newEmail": pending.new_email, 554 + Ok(Json(EmailUpdateStatusOutput { 555 + pending: true, 556 + authorized: pending.authorized, 557 + new_email: Some(pending.new_email), 577 558 })) 578 - .into_response()) 579 559 } 580 560 581 561 #[derive(Deserialize)] ··· 587 567 State(state): State<AppState>, 588 568 _rate_limit: RateLimited<VerificationCheckLimit>, 589 569 Json(input): Json<CheckEmailInUseInput>, 590 - ) -> Response { 570 + ) -> Result<Json<InUseOutput>, ApiError> { 591 571 let email = input.email.trim().to_lowercase(); 592 572 if email.is_empty() { 593 - return ApiError::InvalidRequest("email is required".into()).into_response(); 573 + return Err(ApiError::InvalidRequest("email is required".into())); 594 574 } 595 575 596 - let count = match state.user_repo.count_accounts_by_email(&email).await { 597 - Ok(c) => c, 598 - Err(e) => { 576 + let count = state 577 + .user_repo 578 + .count_accounts_by_email(&email) 579 + .await 580 + .map_err(|e| { 599 581 error!("DB error checking email usage: {:?}", e); 600 - return ApiError::InternalError(None).into_response(); 601 - } 602 - }; 582 + ApiError::InternalError(None) 583 + })?; 603 584 604 - Json(json!({ 605 - "inUse": count > 0, 606 - })) 607 - .into_response() 585 + Ok(Json(InUseOutput { in_use: count > 0 })) 608 586 }
+74 -46
crates/tranquil-api/src/server/meta.rs
··· 1 1 use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; 2 - use serde_json::json; 2 + use serde::Serialize; 3 + use tranquil_db_traits::CommsChannel; 3 4 use tranquil_pds::BUILD_VERSION; 4 5 use tranquil_pds::state::AppState; 5 6 use tranquil_pds::util::{discord_app_id, discord_bot_username, telegram_bot_username}; 6 7 7 - fn get_available_comms_channels() -> Vec<tranquil_db_traits::CommsChannel> { 8 - use tranquil_db_traits::CommsChannel; 8 + fn get_available_comms_channels() -> Vec<CommsChannel> { 9 9 let cfg = tranquil_config::get(); 10 10 let mut channels = vec![CommsChannel::Email]; 11 11 if cfg.discord.bot_token.is_some() { ··· 31 31 tranquil_config::get().server.enable_pds_hosted_did_web 32 32 } 33 33 34 - pub async fn describe_server() -> impl IntoResponse { 34 + #[derive(Serialize)] 35 + #[serde(rename_all = "camelCase")] 36 + pub struct DescribeServerLinks { 37 + #[serde(skip_serializing_if = "Option::is_none")] 38 + pub privacy_policy: Option<String>, 39 + #[serde(skip_serializing_if = "Option::is_none")] 40 + pub terms_of_service: Option<String>, 41 + } 42 + 43 + #[derive(Serialize)] 44 + #[serde(rename_all = "camelCase")] 45 + pub struct DescribeServerContact { 46 + #[serde(skip_serializing_if = "Option::is_none")] 47 + pub email: Option<String>, 48 + } 49 + 50 + #[derive(Serialize)] 51 + #[serde(rename_all = "camelCase")] 52 + pub struct DescribeServerOutput { 53 + pub available_user_domains: Vec<String>, 54 + pub invite_code_required: bool, 55 + pub did: String, 56 + pub links: DescribeServerLinks, 57 + pub contact: DescribeServerContact, 58 + pub version: &'static str, 59 + pub available_comms_channels: Vec<CommsChannel>, 60 + pub self_hosted_did_web_enabled: bool, 61 + #[serde(skip_serializing_if = "Option::is_none")] 62 + pub discord_bot_username: Option<String>, 63 + #[serde(skip_serializing_if = "Option::is_none")] 64 + pub discord_app_id: Option<String>, 65 + #[serde(skip_serializing_if = "Option::is_none")] 66 + pub telegram_bot_username: Option<String>, 67 + } 68 + 69 + pub async fn describe_server() -> Json<DescribeServerOutput> { 35 70 let cfg = tranquil_config::get(); 36 71 let pds_hostname = &cfg.server.hostname; 37 - let domains = cfg.server.user_handle_domain_list(); 38 - let invite_code_required = cfg.server.invite_code_required; 39 - let privacy_policy = cfg.server.privacy_policy_url.clone(); 40 - let terms_of_service = cfg.server.terms_of_service_url.clone(); 41 - let contact_email = cfg.server.contact_email.clone(); 42 - let mut links = serde_json::Map::new(); 43 - if let Some(pp) = privacy_policy { 44 - links.insert("privacyPolicy".to_string(), json!(pp)); 45 - } 46 - if let Some(tos) = terms_of_service { 47 - links.insert("termsOfService".to_string(), json!(tos)); 48 - } 49 - let mut contact = serde_json::Map::new(); 50 - if let Some(email) = contact_email { 51 - contact.insert("email".to_string(), json!(email)); 52 - } 53 - let mut response = json!({ 54 - "availableUserDomains": domains, 55 - "inviteCodeRequired": invite_code_required, 56 - "did": format!("did:web:{}", pds_hostname), 57 - "links": links, 58 - "contact": contact, 59 - "version": BUILD_VERSION, 60 - "availableCommsChannels": get_available_comms_channels(), 61 - "selfHostedDidWebEnabled": is_self_hosted_did_web_enabled() 62 - }); 63 - if let Some(bot_username) = discord_bot_username() { 64 - response["discordBotUsername"] = json!(bot_username); 65 - } 66 - if let Some(app_id) = discord_app_id() { 67 - response["discordAppId"] = json!(app_id); 68 - } 69 - if let Some(bot_username) = telegram_bot_username() { 70 - response["telegramBotUsername"] = json!(bot_username); 71 - } 72 - Json(response) 72 + 73 + Json(DescribeServerOutput { 74 + available_user_domains: cfg.server.user_handle_domain_list(), 75 + invite_code_required: cfg.server.invite_code_required, 76 + did: format!("did:web:{}", pds_hostname), 77 + links: DescribeServerLinks { 78 + privacy_policy: cfg.server.privacy_policy_url.clone(), 79 + terms_of_service: cfg.server.terms_of_service_url.clone(), 80 + }, 81 + contact: DescribeServerContact { 82 + email: cfg.server.contact_email.clone(), 83 + }, 84 + version: BUILD_VERSION, 85 + available_comms_channels: get_available_comms_channels(), 86 + self_hosted_did_web_enabled: is_self_hosted_did_web_enabled(), 87 + discord_bot_username: discord_bot_username().map(String::from), 88 + discord_app_id: discord_app_id().map(String::from), 89 + telegram_bot_username: telegram_bot_username().map(String::from), 90 + }) 91 + } 92 + #[derive(Serialize)] 93 + pub struct HealthOutput { 94 + #[serde(skip_serializing_if = "Option::is_none")] 95 + pub version: Option<String>, 96 + #[serde(skip_serializing_if = "Option::is_none")] 97 + pub error: Option<&'static str>, 73 98 } 99 + 74 100 pub async fn health(State(state): State<AppState>) -> impl IntoResponse { 75 101 match state.infra_repo.health_check().await { 76 102 Ok(true) => ( 77 103 StatusCode::OK, 78 - Json(json!({ 79 - "version": format!("tranquil {}", BUILD_VERSION) 80 - })), 104 + Json(HealthOutput { 105 + version: Some(format!("tranquil {}", BUILD_VERSION)), 106 + error: None, 107 + }), 81 108 ), 82 109 _ => ( 83 110 StatusCode::SERVICE_UNAVAILABLE, 84 - Json(json!({ 85 - "error": "Service Unavailable" 86 - })), 111 + Json(HealthOutput { 112 + version: None, 113 + error: Some("Service Unavailable"), 114 + }), 87 115 ), 88 116 } 89 117 }

History

1 round 0 comments
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints
expand 0 comments
pull request successfully merged