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 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}