scaffold: Backend core — Cargo.toml, main.rs, config, models, panel adapter
Axum server entry point, AppConfig, AppState, ApiError, all model structs (auth, license, server, wipe), and the PanelAdapter trait that abstracts AMP/Pterodactyl/companion connections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
111
backend/src/models/error.rs
Normal file
111
backend/src/models/error.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Unified API error type
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ApiError {
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("Forbidden")]
|
||||
Forbidden,
|
||||
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
#[error("License invalid or expired")]
|
||||
LicenseInvalid,
|
||||
|
||||
#[error("Module not enabled: {0}")]
|
||||
ModuleNotEnabled(String),
|
||||
|
||||
#[error("Rate limited")]
|
||||
RateLimited,
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error(transparent)]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_type, message) = match &self {
|
||||
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()),
|
||||
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()),
|
||||
ApiError::Unauthorized => {
|
||||
(StatusCode::UNAUTHORIZED, "unauthorized", "Unauthorized".to_string())
|
||||
}
|
||||
ApiError::Forbidden => {
|
||||
(StatusCode::FORBIDDEN, "forbidden", "Forbidden".to_string())
|
||||
}
|
||||
ApiError::Conflict(msg) => (StatusCode::CONFLICT, "conflict", msg.clone()),
|
||||
ApiError::LicenseInvalid => (
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"license_invalid",
|
||||
"License invalid or expired".to_string(),
|
||||
),
|
||||
ApiError::ModuleNotEnabled(module) => (
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"module_not_enabled",
|
||||
format!("Module not enabled: {module}"),
|
||||
),
|
||||
ApiError::RateLimited => (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"rate_limited",
|
||||
"Too many requests".to_string(),
|
||||
),
|
||||
ApiError::Internal(msg) => {
|
||||
tracing::error!("Internal error: {msg}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"internal_error",
|
||||
"Internal server error".to_string(),
|
||||
)
|
||||
}
|
||||
ApiError::Database(e) => {
|
||||
tracing::error!("Database error: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"internal_error",
|
||||
"Internal server error".to_string(),
|
||||
)
|
||||
}
|
||||
ApiError::Anyhow(e) => {
|
||||
tracing::error!("Unexpected error: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"internal_error",
|
||||
"Internal server error".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = ErrorResponse {
|
||||
error: error_type.to_string(),
|
||||
message,
|
||||
};
|
||||
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Result alias for API handlers
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
Reference in New Issue
Block a user