this repo has no description
1use axum::{ 2 Json, 3 extract::{FromRequest, Request, rejection::JsonRejection}, 4 http::StatusCode, 5 response::{IntoResponse, Response}, 6}; 7use serde::{Serialize, de::DeserializeOwned}; 8use std::borrow::Cow; 9 10#[derive(Debug, Serialize)] 11struct ErrorBody<'a> { 12 error: Cow<'a, str>, 13 #[serde(skip_serializing_if = "Option::is_none")] 14 message: Option<String>, 15} 16 17#[derive(Debug)] 18pub enum ApiError { 19 InternalError(Option<String>), 20 AuthenticationRequired, 21 AuthenticationFailed(Option<String>), 22 InvalidRequest(String), 23 InvalidToken(Option<String>), 24 ExpiredToken(Option<String>), 25 TokenRequired, 26 AccountDeactivated, 27 AccountTakedown, 28 AccountNotFound, 29 RepoNotFound(Option<String>), 30 RepoTakendown, 31 RepoDeactivated, 32 RecordNotFound, 33 BlobNotFound(Option<String>), 34 InvalidHandle(Option<String>), 35 HandleNotAvailable(Option<String>), 36 HandleTaken, 37 InvalidEmail, 38 EmailTaken, 39 InvalidInviteCode, 40 DuplicateCreate, 41 DuplicateAppPassword, 42 AppPasswordNotFound, 43 SessionNotFound, 44 InvalidSwap(Option<String>), 45 InvalidPassword(String), 46 InvalidRepo(String), 47 AccountMigrated, 48 AccountNotVerified, 49 InvalidCollection, 50 InvalidRecord(String), 51 Forbidden, 52 AdminRequired, 53 InsufficientScope(Option<String>), 54 InvitesDisabled, 55 RateLimitExceeded(Option<String>), 56 PayloadTooLarge(String), 57 TotpAlreadyEnabled, 58 TotpNotEnabled, 59 InvalidCode(Option<String>), 60 InvalidChannel, 61 IdentifierMismatch, 62 NoPasskeys, 63 NoChallengeInProgress, 64 InvalidCredential, 65 PasskeyCounterAnomaly, 66 NoRegistrationInProgress, 67 RegistrationFailed, 68 PasskeyNotFound, 69 InvalidId, 70 InvalidScopes(String), 71 ControllerNotFound, 72 InvalidDelegation(String), 73 DelegationNotFound, 74 InviteCodeRequired, 75 BackupNotFound, 76 BackupsDisabled, 77 RepoNotReady, 78 DeviceNotFound, 79 NoEmail, 80 MfaVerificationRequired, 81 AuthorizationError(String), 82 InvalidDid(String), 83 InvalidSigningKey, 84 SetupExpired, 85 InvalidAccount, 86 InvalidRecoveryLink, 87 RecoveryLinkExpired, 88 MissingEmail, 89 MissingDiscordId, 90 MissingTelegramUsername, 91 MissingSignalNumber, 92 InvalidVerificationChannel, 93 SelfHostedDidWebDisabled, 94 AccountAlreadyExists, 95 HandleNotFound, 96 SubjectNotFound, 97 NotFoundMsg(String), 98 ServiceUnavailable(Option<String>), 99 UpstreamErrorMsg(String), 100 DatabaseError, 101 UpstreamFailure, 102 UpstreamTimeout, 103 UpstreamUnavailable(String), 104 UpstreamError { 105 status: u16, 106 error: Option<String>, 107 message: Option<String>, 108 }, 109} 110 111impl ApiError { 112 fn status_code(&self) -> StatusCode { 113 match self { 114 Self::InternalError(_) | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, 115 Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { 116 StatusCode::BAD_GATEWAY 117 } 118 Self::ServiceUnavailable(_) | Self::BackupsDisabled => StatusCode::SERVICE_UNAVAILABLE, 119 Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT, 120 Self::UpstreamError { status, .. } => { 121 StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY) 122 } 123 Self::AuthenticationRequired 124 | Self::AuthenticationFailed(_) 125 | Self::AccountDeactivated 126 | Self::AccountTakedown 127 | Self::InvalidCode(_) 128 | Self::InvalidPassword(_) 129 | Self::InvalidToken(_) 130 | Self::ExpiredToken(_) 131 | Self::PasskeyCounterAnomaly => StatusCode::UNAUTHORIZED, 132 Self::Forbidden 133 | Self::AdminRequired 134 | Self::InsufficientScope(_) 135 | Self::InvitesDisabled 136 | Self::InvalidRepo(_) 137 | Self::AccountMigrated 138 | Self::AccountNotVerified 139 | Self::MfaVerificationRequired 140 | Self::AuthorizationError(_) => StatusCode::FORBIDDEN, 141 Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS, 142 Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, 143 Self::AccountNotFound 144 | Self::RecordNotFound 145 | Self::AppPasswordNotFound 146 | Self::SessionNotFound 147 | Self::DeviceNotFound 148 | Self::ControllerNotFound 149 | Self::DelegationNotFound 150 | Self::BackupNotFound 151 | Self::InvalidRecoveryLink 152 | Self::HandleNotFound 153 | Self::SubjectNotFound 154 | Self::BlobNotFound(_) 155 | Self::NotFoundMsg(_) => StatusCode::NOT_FOUND, 156 Self::RepoTakendown | Self::RepoDeactivated | Self::RepoNotFound(_) => { 157 StatusCode::BAD_REQUEST 158 } 159 Self::InvalidSwap(_) | Self::TotpAlreadyEnabled => StatusCode::CONFLICT, 160 Self::InvalidRequest(_) 161 | Self::InvalidHandle(_) 162 | Self::HandleNotAvailable(_) 163 | Self::HandleTaken 164 | Self::InvalidEmail 165 | Self::EmailTaken 166 | Self::InvalidInviteCode 167 | Self::DuplicateCreate 168 | Self::DuplicateAppPassword 169 | Self::InvalidCollection 170 | Self::InvalidRecord(_) 171 | Self::TotpNotEnabled 172 | Self::InvalidChannel 173 | Self::IdentifierMismatch 174 | Self::NoPasskeys 175 | Self::NoChallengeInProgress 176 | Self::InvalidCredential 177 | Self::NoEmail 178 | Self::NoRegistrationInProgress 179 | Self::RegistrationFailed 180 | Self::InvalidId 181 | Self::InvalidScopes(_) 182 | Self::InvalidDelegation(_) 183 | Self::InviteCodeRequired 184 | Self::RepoNotReady 185 | Self::InvalidDid(_) 186 | Self::InvalidSigningKey 187 | Self::SetupExpired 188 | Self::InvalidAccount 189 | Self::RecoveryLinkExpired 190 | Self::MissingEmail 191 | Self::MissingDiscordId 192 | Self::MissingTelegramUsername 193 | Self::MissingSignalNumber 194 | Self::InvalidVerificationChannel 195 | Self::SelfHostedDidWebDisabled 196 | Self::AccountAlreadyExists 197 | Self::TokenRequired => StatusCode::BAD_REQUEST, 198 Self::PasskeyNotFound => StatusCode::NOT_FOUND, 199 } 200 } 201 fn error_name(&self) -> Cow<'static, str> { 202 match self { 203 Self::InternalError(_) | Self::DatabaseError => Cow::Borrowed("InternalError"), 204 Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { 205 Cow::Borrowed("UpstreamError") 206 } 207 Self::ServiceUnavailable(_) => Cow::Borrowed("ServiceUnavailable"), 208 Self::NotFoundMsg(_) => Cow::Borrowed("NotFound"), 209 Self::UpstreamTimeout => Cow::Borrowed("UpstreamTimeout"), 210 Self::UpstreamError { error, .. } => { 211 if let Some(e) = error { 212 return Cow::Owned(e.clone()); 213 } 214 Cow::Borrowed("UpstreamError") 215 } 216 Self::AuthenticationRequired => Cow::Borrowed("AuthenticationRequired"), 217 Self::AuthenticationFailed(_) => Cow::Borrowed("AuthenticationFailed"), 218 Self::InvalidToken(_) => Cow::Borrowed("InvalidToken"), 219 Self::ExpiredToken(_) => Cow::Borrowed("ExpiredToken"), 220 Self::TokenRequired => Cow::Borrowed("TokenRequired"), 221 Self::AccountDeactivated => Cow::Borrowed("AccountDeactivated"), 222 Self::AccountTakedown => Cow::Borrowed("AccountTakedown"), 223 Self::Forbidden => Cow::Borrowed("Forbidden"), 224 Self::AdminRequired => Cow::Borrowed("AdminRequired"), 225 Self::InsufficientScope(_) => Cow::Borrowed("InsufficientScope"), 226 Self::InvitesDisabled => Cow::Borrowed("InvitesDisabled"), 227 Self::AccountNotFound => Cow::Borrowed("AccountNotFound"), 228 Self::RepoNotFound(_) => Cow::Borrowed("RepoNotFound"), 229 Self::RepoTakendown => Cow::Borrowed("RepoTakendown"), 230 Self::RepoDeactivated => Cow::Borrowed("RepoDeactivated"), 231 Self::RecordNotFound => Cow::Borrowed("RecordNotFound"), 232 Self::BlobNotFound(_) => Cow::Borrowed("BlobNotFound"), 233 Self::AppPasswordNotFound => Cow::Borrowed("AppPasswordNotFound"), 234 Self::SessionNotFound => Cow::Borrowed("SessionNotFound"), 235 Self::InvalidRequest(_) => Cow::Borrowed("InvalidRequest"), 236 Self::InvalidHandle(_) => Cow::Borrowed("InvalidHandle"), 237 Self::HandleNotAvailable(_) => Cow::Borrowed("HandleNotAvailable"), 238 Self::HandleTaken => Cow::Borrowed("HandleTaken"), 239 Self::InvalidEmail => Cow::Borrowed("InvalidEmail"), 240 Self::EmailTaken => Cow::Borrowed("EmailTaken"), 241 Self::InvalidInviteCode => Cow::Borrowed("InvalidInviteCode"), 242 Self::DuplicateCreate => Cow::Borrowed("DuplicateCreate"), 243 Self::DuplicateAppPassword => Cow::Borrowed("DuplicateAppPassword"), 244 Self::InvalidSwap(_) => Cow::Borrowed("InvalidSwap"), 245 Self::InvalidPassword(_) => Cow::Borrowed("InvalidPassword"), 246 Self::InvalidRepo(_) => Cow::Borrowed("InvalidRepo"), 247 Self::AccountMigrated => Cow::Borrowed("AccountMigrated"), 248 Self::AccountNotVerified => Cow::Borrowed("AccountNotVerified"), 249 Self::InvalidCollection => Cow::Borrowed("InvalidCollection"), 250 Self::InvalidRecord(_) => Cow::Borrowed("InvalidRecord"), 251 Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"), 252 Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"), 253 Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"), 254 Self::InvalidChannel => Cow::Borrowed("InvalidChannel"), 255 Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"), 256 Self::NoPasskeys => Cow::Borrowed("NoPasskeys"), 257 Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"), 258 Self::InvalidCredential => Cow::Borrowed("InvalidCredential"), 259 Self::PasskeyCounterAnomaly => Cow::Borrowed("PasskeyCounterAnomaly"), 260 Self::NoRegistrationInProgress => Cow::Borrowed("NoRegistrationInProgress"), 261 Self::RegistrationFailed => Cow::Borrowed("RegistrationFailed"), 262 Self::PasskeyNotFound => Cow::Borrowed("PasskeyNotFound"), 263 Self::InvalidId => Cow::Borrowed("InvalidId"), 264 Self::InvalidScopes(_) => Cow::Borrowed("InvalidScopes"), 265 Self::ControllerNotFound => Cow::Borrowed("ControllerNotFound"), 266 Self::InvalidDelegation(_) => Cow::Borrowed("InvalidDelegation"), 267 Self::DelegationNotFound => Cow::Borrowed("DelegationNotFound"), 268 Self::InviteCodeRequired => Cow::Borrowed("InviteCodeRequired"), 269 Self::BackupNotFound => Cow::Borrowed("BackupNotFound"), 270 Self::BackupsDisabled => Cow::Borrowed("BackupsDisabled"), 271 Self::RepoNotReady => Cow::Borrowed("RepoNotReady"), 272 Self::MfaVerificationRequired => Cow::Borrowed("MfaVerificationRequired"), 273 Self::RateLimitExceeded(_) => Cow::Borrowed("RateLimitExceeded"), 274 Self::PayloadTooLarge(_) => Cow::Borrowed("PayloadTooLarge"), 275 Self::DeviceNotFound => Cow::Borrowed("DeviceNotFound"), 276 Self::NoEmail => Cow::Borrowed("NoEmail"), 277 Self::AuthorizationError(_) => Cow::Borrowed("AuthorizationError"), 278 Self::InvalidDid(_) => Cow::Borrowed("InvalidDid"), 279 Self::InvalidSigningKey => Cow::Borrowed("InvalidSigningKey"), 280 Self::SetupExpired => Cow::Borrowed("SetupExpired"), 281 Self::InvalidAccount => Cow::Borrowed("InvalidAccount"), 282 Self::InvalidRecoveryLink => Cow::Borrowed("InvalidRecoveryLink"), 283 Self::RecoveryLinkExpired => Cow::Borrowed("RecoveryLinkExpired"), 284 Self::MissingEmail => Cow::Borrowed("MissingEmail"), 285 Self::MissingDiscordId => Cow::Borrowed("MissingDiscordId"), 286 Self::MissingTelegramUsername => Cow::Borrowed("MissingTelegramUsername"), 287 Self::MissingSignalNumber => Cow::Borrowed("MissingSignalNumber"), 288 Self::InvalidVerificationChannel => Cow::Borrowed("InvalidVerificationChannel"), 289 Self::SelfHostedDidWebDisabled => Cow::Borrowed("SelfHostedDidWebDisabled"), 290 Self::AccountAlreadyExists => Cow::Borrowed("AccountAlreadyExists"), 291 Self::HandleNotFound => Cow::Borrowed("HandleNotFound"), 292 Self::SubjectNotFound => Cow::Borrowed("SubjectNotFound"), 293 } 294 } 295 fn message(&self) -> Option<String> { 296 match self { 297 Self::InternalError(msg) 298 | Self::AuthenticationFailed(msg) 299 | Self::InvalidToken(msg) 300 | Self::ExpiredToken(msg) 301 | Self::RepoNotFound(msg) 302 | Self::BlobNotFound(msg) 303 | Self::InvalidHandle(msg) 304 | Self::HandleNotAvailable(msg) 305 | Self::InvalidSwap(msg) 306 | Self::InsufficientScope(msg) 307 | Self::InvalidCode(msg) 308 | Self::RateLimitExceeded(msg) 309 | Self::ServiceUnavailable(msg) => msg.clone(), 310 Self::InvalidRequest(msg) 311 | Self::UpstreamUnavailable(msg) 312 | Self::InvalidPassword(msg) 313 | Self::InvalidRepo(msg) 314 | Self::InvalidRecord(msg) 315 | Self::NotFoundMsg(msg) 316 | Self::UpstreamErrorMsg(msg) 317 | Self::PayloadTooLarge(msg) => Some(msg.clone()), 318 Self::AccountMigrated => Some( 319 "Account has been migrated to another PDS. Repo operations are not allowed." 320 .to_string(), 321 ), 322 Self::AccountNotVerified => Some( 323 "You must verify at least one notification channel before creating records" 324 .to_string(), 325 ), 326 Self::NoPasskeys => { 327 Some("No passkeys registered for this account".to_string()) 328 } 329 Self::NoChallengeInProgress => Some( 330 "No passkey authentication in progress or challenge expired".to_string(), 331 ), 332 Self::InvalidCredential => Some("Failed to parse credential response".to_string()), 333 Self::NoRegistrationInProgress => Some( 334 "No registration in progress. Call startPasskeyRegistration first.".to_string(), 335 ), 336 Self::RegistrationFailed => { 337 Some("Failed to verify passkey registration".to_string()) 338 } 339 Self::PasskeyNotFound => Some("Passkey not found".to_string()), 340 Self::InvalidId => Some("Invalid ID format".to_string()), 341 Self::InvalidScopes(msg) | Self::InvalidDelegation(msg) => Some(msg.clone()), 342 Self::ControllerNotFound => Some("Controller account not found".to_string()), 343 Self::DelegationNotFound => { 344 Some("No active delegation found for this controller".to_string()) 345 } 346 Self::InviteCodeRequired => { 347 Some("An invite code is required to create an account".to_string()) 348 } 349 Self::BackupNotFound => Some("Backup not found".to_string()), 350 Self::BackupsDisabled => Some("Backup storage not configured".to_string()), 351 Self::RepoNotReady => Some("Repository not ready for backup".to_string()), 352 Self::PasskeyCounterAnomaly => Some( 353 "Authentication failed: security key counter anomaly detected. This may indicate a cloned key.".to_string(), 354 ), 355 Self::MfaVerificationRequired => Some( 356 "This sensitive operation requires MFA verification".to_string(), 357 ), 358 Self::DeviceNotFound => Some("Device not found".to_string()), 359 Self::NoEmail => Some("Recipient has no email address".to_string()), 360 Self::AuthorizationError(msg) | Self::InvalidDid(msg) => Some(msg.clone()), 361 Self::InvalidSigningKey => { 362 Some("Signing key not found, already used, or expired".to_string()) 363 } 364 Self::SetupExpired => { 365 Some("Setup has already been completed or expired".to_string()) 366 } 367 Self::InvalidAccount => { 368 Some("This account is not a passkey-only account".to_string()) 369 } 370 Self::InvalidRecoveryLink => Some("Invalid recovery link".to_string()), 371 Self::RecoveryLinkExpired => Some("Recovery link has expired".to_string()), 372 Self::MissingEmail => { 373 Some("Email is required when using email verification".to_string()) 374 } 375 Self::MissingDiscordId => { 376 Some("Discord ID is required when using Discord verification".to_string()) 377 } 378 Self::MissingTelegramUsername => { 379 Some("Telegram username is required when using Telegram verification".to_string()) 380 } 381 Self::MissingSignalNumber => { 382 Some("Signal phone number is required when using Signal verification".to_string()) 383 } 384 Self::InvalidVerificationChannel => Some("Invalid verification channel".to_string()), 385 Self::SelfHostedDidWebDisabled => { 386 Some("Self-hosted did:web accounts are disabled on this server".to_string()) 387 } 388 Self::AccountAlreadyExists => Some("Account already exists".to_string()), 389 Self::HandleNotFound => Some("Unable to resolve handle".to_string()), 390 Self::SubjectNotFound => Some("Subject not found".to_string()), 391 Self::IdentifierMismatch => { 392 Some("The identifier does not match the verification token".to_string()) 393 } 394 Self::UpstreamError { message, .. } => message.clone(), 395 Self::UpstreamTimeout => Some("Upstream service timed out".to_string()), 396 Self::AdminRequired => Some("This action requires admin privileges".to_string()), 397 _ => None, 398 } 399 } 400 pub fn from_upstream_response(status: u16, body: &[u8]) -> Self { 401 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(body) { 402 let error = parsed 403 .get("error") 404 .and_then(|v| v.as_str()) 405 .map(String::from); 406 let message = parsed 407 .get("message") 408 .and_then(|v| v.as_str()) 409 .map(String::from); 410 return Self::UpstreamError { 411 status, 412 error, 413 message, 414 }; 415 } 416 Self::UpstreamError { 417 status, 418 error: None, 419 message: None, 420 } 421 } 422} 423 424impl IntoResponse for ApiError { 425 fn into_response(self) -> Response { 426 let body = ErrorBody { 427 error: self.error_name(), 428 message: self.message(), 429 }; 430 (self.status_code(), Json(body)).into_response() 431 } 432} 433 434impl From<sqlx::Error> for ApiError { 435 fn from(e: sqlx::Error) -> Self { 436 tracing::error!("Database error: {:?}", e); 437 Self::DatabaseError 438 } 439} 440 441impl From<crate::auth::TokenValidationError> for ApiError { 442 fn from(e: crate::auth::TokenValidationError) -> Self { 443 match e { 444 crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated, 445 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown, 446 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError(None), 447 crate::auth::TokenValidationError::AuthenticationFailed => { 448 Self::AuthenticationFailed(None) 449 } 450 crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken(None), 451 } 452 } 453} 454 455impl From<crate::util::DbLookupError> for ApiError { 456 fn from(e: crate::util::DbLookupError) -> Self { 457 match e { 458 crate::util::DbLookupError::NotFound => Self::AccountNotFound, 459 crate::util::DbLookupError::DatabaseError(db_err) => { 460 tracing::error!("Database error: {:?}", db_err); 461 Self::DatabaseError 462 } 463 } 464 } 465} 466 467impl From<crate::auth::extractor::AuthError> for ApiError { 468 fn from(e: crate::auth::extractor::AuthError) -> Self { 469 match e { 470 crate::auth::extractor::AuthError::MissingToken => Self::AuthenticationRequired, 471 crate::auth::extractor::AuthError::InvalidFormat => { 472 Self::AuthenticationFailed(Some("Invalid authorization header format".to_string())) 473 } 474 crate::auth::extractor::AuthError::AuthenticationFailed => { 475 Self::AuthenticationFailed(None) 476 } 477 crate::auth::extractor::AuthError::TokenExpired => { 478 Self::ExpiredToken(Some("Token has expired".to_string())) 479 } 480 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 481 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 482 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, 483 } 484 } 485} 486 487impl From<crate::handle::HandleResolutionError> for ApiError { 488 fn from(e: crate::handle::HandleResolutionError) -> Self { 489 match e { 490 crate::handle::HandleResolutionError::NotFound => Self::HandleNotFound, 491 crate::handle::HandleResolutionError::InvalidDid => { 492 Self::InvalidHandle(Some("Invalid DID format in handle record".to_string())) 493 } 494 crate::handle::HandleResolutionError::DidMismatch { expected, actual } => { 495 Self::InvalidHandle(Some(format!( 496 "Handle DID mismatch: expected {}, got {}", 497 expected, actual 498 ))) 499 } 500 crate::handle::HandleResolutionError::DnsError(msg) => { 501 Self::InternalError(Some(format!("DNS resolution failed: {}", msg))) 502 } 503 crate::handle::HandleResolutionError::HttpError(msg) => { 504 Self::InternalError(Some(format!("Handle HTTP resolution failed: {}", msg))) 505 } 506 } 507 } 508} 509 510impl From<crate::auth::verification_token::VerifyError> for ApiError { 511 fn from(e: crate::auth::verification_token::VerifyError) -> Self { 512 use crate::auth::verification_token::VerifyError; 513 match e { 514 VerifyError::InvalidFormat => { 515 Self::InvalidRequest("The verification code is invalid or malformed".to_string()) 516 } 517 VerifyError::UnsupportedVersion => { 518 Self::InvalidRequest("This verification code version is not supported".to_string()) 519 } 520 VerifyError::Expired => Self::InvalidRequest( 521 "The verification code has expired. Please request a new one.".to_string(), 522 ), 523 VerifyError::InvalidSignature => { 524 Self::InvalidRequest("The verification code is invalid".to_string()) 525 } 526 VerifyError::IdentifierMismatch => Self::IdentifierMismatch, 527 VerifyError::PurposeMismatch => { 528 Self::InvalidRequest("Verification code purpose does not match".to_string()) 529 } 530 VerifyError::ChannelMismatch => { 531 Self::InvalidRequest("Verification code channel does not match".to_string()) 532 } 533 } 534 } 535} 536 537impl From<crate::api::validation::HandleValidationError> for ApiError { 538 fn from(e: crate::api::validation::HandleValidationError) -> Self { 539 use crate::api::validation::HandleValidationError; 540 match e { 541 HandleValidationError::Reserved => Self::HandleNotAvailable(None), 542 HandleValidationError::BannedWord => { 543 Self::InvalidHandle(Some("Inappropriate language in handle".to_string())) 544 } 545 _ => Self::InvalidHandle(Some(e.to_string())), 546 } 547 } 548} 549 550impl From<jacquard::types::string::AtStrError> for ApiError { 551 fn from(e: jacquard::types::string::AtStrError) -> Self { 552 Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind)) 553 } 554} 555 556impl From<crate::plc::PlcError> for ApiError { 557 fn from(e: crate::plc::PlcError) -> Self { 558 use crate::plc::PlcError; 559 match e { 560 PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()), 561 PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()), 562 PlcError::Timeout => Self::UpstreamTimeout, 563 PlcError::CircuitBreakerOpen => Self::ServiceUnavailable(Some( 564 "PLC directory service temporarily unavailable".into(), 565 )), 566 PlcError::Http(err) => { 567 tracing::error!("PLC HTTP error: {:?}", err); 568 Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into()) 569 } 570 PlcError::InvalidResponse(msg) => { 571 tracing::error!("PLC invalid response: {}", msg); 572 Self::UpstreamErrorMsg(format!("Invalid response from PLC directory: {}", msg)) 573 } 574 PlcError::Serialization(msg) => { 575 tracing::error!("PLC serialization error: {}", msg); 576 Self::InternalError(Some(format!("PLC serialization error: {}", msg))) 577 } 578 PlcError::Signing(msg) => { 579 tracing::error!("PLC signing error: {}", msg); 580 Self::InternalError(Some(format!("PLC signing error: {}", msg))) 581 } 582 } 583 } 584} 585 586impl From<bcrypt::BcryptError> for ApiError { 587 fn from(e: bcrypt::BcryptError) -> Self { 588 tracing::error!("Bcrypt error: {:?}", e); 589 Self::InternalError(None) 590 } 591} 592 593impl From<cid::Error> for ApiError { 594 fn from(e: cid::Error) -> Self { 595 Self::InvalidRequest(format!("Invalid CID: {}", e)) 596 } 597} 598 599impl From<crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>> for ApiError { 600 fn from(e: crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>) -> Self { 601 use crate::circuit_breaker::CircuitBreakerError; 602 match e { 603 CircuitBreakerError::CircuitOpen(err) => { 604 tracing::warn!("PLC directory circuit breaker open: {}", err); 605 Self::ServiceUnavailable(Some( 606 "PLC directory service temporarily unavailable".into(), 607 )) 608 } 609 CircuitBreakerError::OperationFailed(plc_err) => Self::from(plc_err), 610 } 611 } 612} 613 614impl From<crate::storage::StorageError> for ApiError { 615 fn from(e: crate::storage::StorageError) -> Self { 616 tracing::error!("Storage error: {:?}", e); 617 Self::InternalError(Some("Storage operation failed".into())) 618 } 619} 620 621pub struct AtpJson<T>(pub T); 622 623impl<T, S> FromRequest<S> for AtpJson<T> 624where 625 T: DeserializeOwned, 626 S: Send + Sync, 627{ 628 type Rejection = (StatusCode, Json<serde_json::Value>); 629 630 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> { 631 match Json::<T>::from_request(req, state).await { 632 Ok(Json(value)) => Ok(AtpJson(value)), 633 Err(rejection) => { 634 let message = extract_json_error_message(&rejection); 635 Err(( 636 StatusCode::BAD_REQUEST, 637 Json(serde_json::json!({ 638 "error": "InvalidRequest", 639 "message": message 640 })), 641 )) 642 } 643 } 644 } 645} 646 647fn extract_json_error_message(rejection: &JsonRejection) -> String { 648 match rejection { 649 JsonRejection::JsonDataError(e) => { 650 let inner = e.body_text(); 651 if inner.contains("missing field") { 652 let field = inner 653 .split("missing field `") 654 .nth(1) 655 .and_then(|s| s.split('`').next()) 656 .unwrap_or("unknown"); 657 format!("Missing required field: {}", field) 658 } else if inner.contains("invalid type") { 659 format!("Invalid field type: {}", inner) 660 } else { 661 inner 662 } 663 } 664 JsonRejection::JsonSyntaxError(_) => "Invalid JSON syntax".to_string(), 665 JsonRejection::MissingJsonContentType(_) => { 666 "Content-Type must be application/json".to_string() 667 } 668 JsonRejection::BytesRejection(_) => "Failed to read request body".to_string(), 669 _ => "Invalid request body".to_string(), 670 } 671}