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