1use axum::{
25 http::StatusCode,
26 response::{IntoResponse, Response},
27 Json,
28};
29use serde::{Deserialize, Serialize};
30
31#[cfg(feature = "openapi")]
32use utoipa::ToSchema;
33
34use crate::errors::LicenseError;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[cfg_attr(feature = "openapi", derive(ToSchema))]
41#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
42pub enum ErrorCode {
43 LicenseNotFound,
46 LicenseExpired,
48 LicenseRevoked,
50 LicenseSuspended,
52 LicenseBlacklisted,
54 LicenseInactive,
56
57 AlreadyBound,
60 NotBound,
62 HardwareMismatch,
64
65 FeatureNotIncluded,
68 QuotaExceeded,
70
71 InvalidRequest,
74 MissingField,
76 InvalidField,
78
79 MissingToken,
82 InvalidHeader,
84 InvalidToken,
86 TokenExpired,
88 InsufficientScope,
90 AuthDisabled,
92
93 NotFound,
96 Conflict,
98
99 DatabaseError,
102 ConfigError,
104 CryptoError,
106 NetworkError,
108 InternalError,
110}
111
112impl ErrorCode {
113 pub fn status_code(&self) -> StatusCode {
115 match self {
116 ErrorCode::InvalidRequest
118 | ErrorCode::MissingField
119 | ErrorCode::InvalidField
120 | ErrorCode::InvalidHeader => StatusCode::BAD_REQUEST,
121
122 ErrorCode::MissingToken | ErrorCode::InvalidToken | ErrorCode::TokenExpired => {
124 StatusCode::UNAUTHORIZED
125 }
126
127 ErrorCode::LicenseExpired
129 | ErrorCode::LicenseRevoked
130 | ErrorCode::LicenseSuspended
131 | ErrorCode::LicenseBlacklisted
132 | ErrorCode::LicenseInactive
133 | ErrorCode::HardwareMismatch
134 | ErrorCode::FeatureNotIncluded
135 | ErrorCode::QuotaExceeded
136 | ErrorCode::InsufficientScope => StatusCode::FORBIDDEN,
137
138 ErrorCode::LicenseNotFound | ErrorCode::NotFound => StatusCode::NOT_FOUND,
140
141 ErrorCode::AlreadyBound | ErrorCode::NotBound | ErrorCode::Conflict => {
143 StatusCode::CONFLICT
144 }
145
146 ErrorCode::DatabaseError
148 | ErrorCode::ConfigError
149 | ErrorCode::CryptoError
150 | ErrorCode::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
151
152 ErrorCode::AuthDisabled => StatusCode::NOT_IMPLEMENTED,
154
155 ErrorCode::NetworkError => StatusCode::BAD_GATEWAY,
157 }
158 }
159
160 pub fn default_message(&self) -> &'static str {
162 match self {
163 ErrorCode::LicenseNotFound => "The requested license does not exist",
164 ErrorCode::LicenseExpired => "License has expired",
165 ErrorCode::LicenseRevoked => "License has been revoked",
166 ErrorCode::LicenseSuspended => "License is temporarily suspended",
167 ErrorCode::LicenseBlacklisted => "License has been permanently blacklisted",
168 ErrorCode::LicenseInactive => "License is not active",
169 ErrorCode::AlreadyBound => "License is already bound to another device",
170 ErrorCode::NotBound => "License is not bound to any device",
171 ErrorCode::HardwareMismatch => "Hardware ID does not match the bound device",
172 ErrorCode::FeatureNotIncluded => "Feature is not included in your license tier",
173 ErrorCode::QuotaExceeded => "Usage quota has been exceeded",
174 ErrorCode::InvalidRequest => "Request payload is invalid",
175 ErrorCode::MissingField => "A required field is missing",
176 ErrorCode::InvalidField => "A field value is invalid",
177 ErrorCode::MissingToken => "Authentication token is required",
178 ErrorCode::InvalidHeader => "Authorization header is malformed",
179 ErrorCode::InvalidToken => "Authentication token is invalid",
180 ErrorCode::TokenExpired => "Authentication token has expired",
181 ErrorCode::InsufficientScope => "Insufficient permissions for this operation",
182 ErrorCode::AuthDisabled => "Authentication is not configured on this server",
183 ErrorCode::NotFound => "The requested resource was not found",
184 ErrorCode::Conflict => "Operation conflicts with current resource state",
185 ErrorCode::DatabaseError => "Database operation failed",
186 ErrorCode::ConfigError => "Server configuration error",
187 ErrorCode::CryptoError => "Encryption operation failed",
188 ErrorCode::NetworkError => "Failed to communicate with external service",
189 ErrorCode::InternalError => "An unexpected error occurred",
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[cfg_attr(feature = "openapi", derive(ToSchema))]
197pub struct ErrorBody {
198 pub code: ErrorCode,
200 pub message: String,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub details: Option<serde_json::Value>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
211#[cfg_attr(feature = "openapi", derive(ToSchema))]
212pub struct ApiError {
213 pub error: ErrorBody,
215}
216
217impl ApiError {
218 pub fn new(code: ErrorCode) -> Self {
222 Self {
223 error: ErrorBody {
224 code,
225 message: code.default_message().to_string(),
226 details: None,
227 },
228 }
229 }
230
231 pub fn with_message(code: ErrorCode, message: impl Into<String>) -> Self {
233 Self {
234 error: ErrorBody {
235 code,
236 message: message.into(),
237 details: None,
238 },
239 }
240 }
241
242 pub fn with_details(
244 code: ErrorCode,
245 message: impl Into<String>,
246 details: serde_json::Value,
247 ) -> Self {
248 Self {
249 error: ErrorBody {
250 code,
251 message: message.into(),
252 details: Some(details),
253 },
254 }
255 }
256
257 pub fn details(mut self, details: serde_json::Value) -> Self {
259 self.error.details = Some(details);
260 self
261 }
262
263 pub fn status_code(&self) -> StatusCode {
265 self.error.code.status_code()
266 }
267
268 pub fn license_not_found() -> Self {
272 Self::new(ErrorCode::LicenseNotFound)
273 }
274
275 pub fn license_not_found_key(key: &str) -> Self {
277 Self::with_message(
278 ErrorCode::LicenseNotFound,
279 format!("License '{}' not found", key),
280 )
281 }
282
283 pub fn invalid_field(field: &str, reason: &str) -> Self {
285 Self::with_details(
286 ErrorCode::InvalidField,
287 format!("Invalid value for '{}': {}", field, reason),
288 serde_json::json!({ "field": field }),
289 )
290 }
291
292 pub fn missing_field(field: &str) -> Self {
294 Self::with_details(
295 ErrorCode::MissingField,
296 format!("Required field '{}' is missing", field),
297 serde_json::json!({ "field": field }),
298 )
299 }
300
301 pub fn not_found(resource: &str) -> Self {
303 Self::with_message(ErrorCode::NotFound, format!("{} not found", resource))
304 }
305
306 pub fn database_error() -> Self {
308 Self::new(ErrorCode::DatabaseError)
309 }
310
311 pub fn internal_error() -> Self {
313 Self::new(ErrorCode::InternalError)
314 }
315}
316
317impl IntoResponse for ApiError {
318 fn into_response(self) -> Response {
319 let status = self.status_code();
320 (status, Json(self)).into_response()
321 }
322}
323
324impl std::fmt::Display for ApiError {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 write!(
327 f,
328 "{}: {}",
329 self.error.code.default_message(),
330 self.error.message
331 )
332 }
333}
334
335impl std::error::Error for ApiError {}
336
337impl From<LicenseError> for ApiError {
340 fn from(err: LicenseError) -> Self {
341 match err {
342 LicenseError::InvalidLicense(msg) => {
343 ApiError::with_message(ErrorCode::InvalidRequest, msg)
344 }
345 LicenseError::ConfigError(msg) => ApiError::with_message(ErrorCode::ConfigError, msg),
346 LicenseError::NetworkError(e) => {
347 ApiError::with_message(ErrorCode::NetworkError, e.to_string())
348 }
349 LicenseError::StorageError(e) => {
350 ApiError::with_message(ErrorCode::InternalError, e.to_string())
351 }
352 LicenseError::EncryptionError(msg)
353 | LicenseError::DecryptionError(msg)
354 | LicenseError::KeyringError(msg) => {
355 ApiError::with_message(ErrorCode::CryptoError, msg)
356 }
357 LicenseError::ServerError(msg) => ApiError::with_message(ErrorCode::InternalError, msg),
358 LicenseError::UnknownError => ApiError::new(ErrorCode::InternalError),
359 LicenseError::ClientApiError(e) => {
360 use crate::client::errors::ClientErrorCode;
362 let code = match e.code {
363 ClientErrorCode::LicenseNotFound => ErrorCode::LicenseNotFound,
364 ClientErrorCode::LicenseExpired => ErrorCode::LicenseExpired,
365 ClientErrorCode::LicenseRevoked => ErrorCode::LicenseRevoked,
366 ClientErrorCode::LicenseSuspended => ErrorCode::LicenseSuspended,
367 ClientErrorCode::LicenseBlacklisted => ErrorCode::LicenseBlacklisted,
368 ClientErrorCode::LicenseInactive => ErrorCode::LicenseInactive,
369 ClientErrorCode::AlreadyBound => ErrorCode::AlreadyBound,
370 ClientErrorCode::NotBound => ErrorCode::NotBound,
371 ClientErrorCode::HardwareMismatch => ErrorCode::HardwareMismatch,
372 ClientErrorCode::FeatureNotIncluded => ErrorCode::FeatureNotIncluded,
373 ClientErrorCode::QuotaExceeded => ErrorCode::QuotaExceeded,
374 ClientErrorCode::GracePeriodExpired
375 | ClientErrorCode::InternalError
376 | ClientErrorCode::Unknown => ErrorCode::InternalError,
377 };
378 ApiError::with_message(code, e.message)
379 }
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn error_code_status_mapping() {
390 assert_eq!(
391 ErrorCode::LicenseNotFound.status_code(),
392 StatusCode::NOT_FOUND
393 );
394 assert_eq!(
395 ErrorCode::InvalidRequest.status_code(),
396 StatusCode::BAD_REQUEST
397 );
398 assert_eq!(
399 ErrorCode::MissingToken.status_code(),
400 StatusCode::UNAUTHORIZED
401 );
402 assert_eq!(
403 ErrorCode::LicenseExpired.status_code(),
404 StatusCode::FORBIDDEN
405 );
406 assert_eq!(ErrorCode::AlreadyBound.status_code(), StatusCode::CONFLICT);
407 assert_eq!(
408 ErrorCode::DatabaseError.status_code(),
409 StatusCode::INTERNAL_SERVER_ERROR
410 );
411 }
412
413 #[test]
414 fn api_error_serialization() {
415 let err = ApiError::license_not_found();
416 let json = serde_json::to_string(&err).unwrap();
417 assert!(json.contains("LICENSE_NOT_FOUND"));
418 assert!(json.contains("message"));
419 }
420
421 #[test]
422 fn api_error_with_details() {
423 let err = ApiError::invalid_field("email", "must be a valid email address");
424 let json = serde_json::to_string(&err).unwrap();
425 assert!(json.contains("INVALID_FIELD"));
426 assert!(json.contains("email"));
427 }
428
429 #[test]
430 fn license_error_conversion() {
431 let license_err = LicenseError::InvalidLicense("bad key".to_string());
432 let api_err: ApiError = license_err.into();
433 assert_eq!(api_err.error.code, ErrorCode::InvalidRequest);
434 }
435}