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::PasskeyCounterAnomaly => StatusCode::UNAUTHORIZED,
131 Self::ExpiredToken(_) => StatusCode::BAD_REQUEST,
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 let mut response = (self.status_code(), Json(body)).into_response();
431 if matches!(self, Self::ExpiredToken(_)) {
432 response.headers_mut().insert(
433 "WWW-Authenticate",
434 "Bearer error=\"invalid_token\", error_description=\"Token has expired\""
435 .parse()
436 .unwrap(),
437 );
438 }
439 response
440 }
441}
442
443impl From<sqlx::Error> for ApiError {
444 fn from(e: sqlx::Error) -> Self {
445 tracing::error!("Database error: {:?}", e);
446 Self::DatabaseError
447 }
448}
449
450impl From<crate::auth::TokenValidationError> for ApiError {
451 fn from(e: crate::auth::TokenValidationError) -> Self {
452 match e {
453 crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated,
454 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown,
455 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError(None),
456 crate::auth::TokenValidationError::AuthenticationFailed => {
457 Self::AuthenticationFailed(None)
458 }
459 crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken(None),
460 }
461 }
462}
463
464impl From<crate::util::DbLookupError> for ApiError {
465 fn from(e: crate::util::DbLookupError) -> Self {
466 match e {
467 crate::util::DbLookupError::NotFound => Self::AccountNotFound,
468 crate::util::DbLookupError::DatabaseError(db_err) => {
469 tracing::error!("Database error: {:?}", db_err);
470 Self::DatabaseError
471 }
472 }
473 }
474}
475
476impl From<crate::auth::extractor::AuthError> for ApiError {
477 fn from(e: crate::auth::extractor::AuthError) -> Self {
478 match e {
479 crate::auth::extractor::AuthError::MissingToken => Self::AuthenticationRequired,
480 crate::auth::extractor::AuthError::InvalidFormat => {
481 Self::AuthenticationFailed(Some("Invalid authorization header format".to_string()))
482 }
483 crate::auth::extractor::AuthError::AuthenticationFailed => {
484 Self::AuthenticationFailed(None)
485 }
486 crate::auth::extractor::AuthError::TokenExpired => {
487 Self::ExpiredToken(Some("Token has expired".to_string()))
488 }
489 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated,
490 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown,
491 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired,
492 }
493 }
494}
495
496impl From<crate::handle::HandleResolutionError> for ApiError {
497 fn from(e: crate::handle::HandleResolutionError) -> Self {
498 match e {
499 crate::handle::HandleResolutionError::NotFound => Self::HandleNotFound,
500 crate::handle::HandleResolutionError::InvalidDid => {
501 Self::InvalidHandle(Some("Invalid DID format in handle record".to_string()))
502 }
503 crate::handle::HandleResolutionError::DidMismatch { expected, actual } => {
504 Self::InvalidHandle(Some(format!(
505 "Handle DID mismatch: expected {}, got {}",
506 expected, actual
507 )))
508 }
509 crate::handle::HandleResolutionError::DnsError(msg) => {
510 Self::InternalError(Some(format!("DNS resolution failed: {}", msg)))
511 }
512 crate::handle::HandleResolutionError::HttpError(msg) => {
513 Self::InternalError(Some(format!("Handle HTTP resolution failed: {}", msg)))
514 }
515 }
516 }
517}
518
519impl From<crate::auth::verification_token::VerifyError> for ApiError {
520 fn from(e: crate::auth::verification_token::VerifyError) -> Self {
521 use crate::auth::verification_token::VerifyError;
522 match e {
523 VerifyError::InvalidFormat => {
524 Self::InvalidRequest("The verification code is invalid or malformed".to_string())
525 }
526 VerifyError::UnsupportedVersion => {
527 Self::InvalidRequest("This verification code version is not supported".to_string())
528 }
529 VerifyError::Expired => Self::InvalidRequest(
530 "The verification code has expired. Please request a new one.".to_string(),
531 ),
532 VerifyError::InvalidSignature => {
533 Self::InvalidRequest("The verification code is invalid".to_string())
534 }
535 VerifyError::IdentifierMismatch => Self::IdentifierMismatch,
536 VerifyError::PurposeMismatch => {
537 Self::InvalidRequest("Verification code purpose does not match".to_string())
538 }
539 VerifyError::ChannelMismatch => {
540 Self::InvalidRequest("Verification code channel does not match".to_string())
541 }
542 }
543 }
544}
545
546impl From<crate::api::validation::HandleValidationError> for ApiError {
547 fn from(e: crate::api::validation::HandleValidationError) -> Self {
548 use crate::api::validation::HandleValidationError;
549 match e {
550 HandleValidationError::Reserved => Self::HandleNotAvailable(None),
551 HandleValidationError::BannedWord => {
552 Self::InvalidHandle(Some("Inappropriate language in handle".to_string()))
553 }
554 _ => Self::InvalidHandle(Some(e.to_string())),
555 }
556 }
557}
558
559impl From<jacquard::types::string::AtStrError> for ApiError {
560 fn from(e: jacquard::types::string::AtStrError) -> Self {
561 Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind))
562 }
563}
564
565impl From<crate::plc::PlcError> for ApiError {
566 fn from(e: crate::plc::PlcError) -> Self {
567 use crate::plc::PlcError;
568 match e {
569 PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()),
570 PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()),
571 PlcError::Timeout => Self::UpstreamTimeout,
572 PlcError::CircuitBreakerOpen => Self::ServiceUnavailable(Some(
573 "PLC directory service temporarily unavailable".into(),
574 )),
575 PlcError::Http(err) => {
576 tracing::error!("PLC HTTP error: {:?}", err);
577 Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into())
578 }
579 PlcError::InvalidResponse(msg) => {
580 tracing::error!("PLC invalid response: {}", msg);
581 Self::UpstreamErrorMsg(format!("Invalid response from PLC directory: {}", msg))
582 }
583 PlcError::Serialization(msg) => {
584 tracing::error!("PLC serialization error: {}", msg);
585 Self::InternalError(Some(format!("PLC serialization error: {}", msg)))
586 }
587 PlcError::Signing(msg) => {
588 tracing::error!("PLC signing error: {}", msg);
589 Self::InternalError(Some(format!("PLC signing error: {}", msg)))
590 }
591 }
592 }
593}
594
595impl From<bcrypt::BcryptError> for ApiError {
596 fn from(e: bcrypt::BcryptError) -> Self {
597 tracing::error!("Bcrypt error: {:?}", e);
598 Self::InternalError(None)
599 }
600}
601
602impl From<cid::Error> for ApiError {
603 fn from(e: cid::Error) -> Self {
604 Self::InvalidRequest(format!("Invalid CID: {}", e))
605 }
606}
607
608impl From<crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>> for ApiError {
609 fn from(e: crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>) -> Self {
610 use crate::circuit_breaker::CircuitBreakerError;
611 match e {
612 CircuitBreakerError::CircuitOpen(err) => {
613 tracing::warn!("PLC directory circuit breaker open: {}", err);
614 Self::ServiceUnavailable(Some(
615 "PLC directory service temporarily unavailable".into(),
616 ))
617 }
618 CircuitBreakerError::OperationFailed(plc_err) => Self::from(plc_err),
619 }
620 }
621}
622
623impl From<crate::storage::StorageError> for ApiError {
624 fn from(e: crate::storage::StorageError) -> Self {
625 tracing::error!("Storage error: {:?}", e);
626 Self::InternalError(Some("Storage operation failed".into()))
627 }
628}
629
630pub struct AtpJson<T>(pub T);
631
632impl<T, S> FromRequest<S> for AtpJson<T>
633where
634 T: DeserializeOwned,
635 S: Send + Sync,
636{
637 type Rejection = (StatusCode, Json<serde_json::Value>);
638
639 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
640 match Json::<T>::from_request(req, state).await {
641 Ok(Json(value)) => Ok(AtpJson(value)),
642 Err(rejection) => {
643 let message = extract_json_error_message(&rejection);
644 Err((
645 StatusCode::BAD_REQUEST,
646 Json(serde_json::json!({
647 "error": "InvalidRequest",
648 "message": message
649 })),
650 ))
651 }
652 }
653 }
654}
655
656fn extract_json_error_message(rejection: &JsonRejection) -> String {
657 match rejection {
658 JsonRejection::JsonDataError(e) => {
659 let inner = e.body_text();
660 if inner.contains("missing field") {
661 let field = inner
662 .split("missing field `")
663 .nth(1)
664 .and_then(|s| s.split('`').next())
665 .unwrap_or("unknown");
666 format!("Missing required field: {}", field)
667 } else if inner.contains("invalid type") {
668 format!("Invalid field type: {}", inner)
669 } else {
670 inner
671 }
672 }
673 JsonRejection::JsonSyntaxError(_) => "Invalid JSON syntax".to_string(),
674 JsonRejection::MissingJsonContentType(_) => {
675 "Content-Type must be application/json".to_string()
676 }
677 JsonRejection::BytesRejection(_) => "Failed to read request body".to_string(),
678 _ => "Invalid request body".to_string(),
679 }
680}