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}