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