Files
funman300 6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 13:07:22 -07:00

98 lines
3.1 KiB
Rust

//! Application-level error type with automatic HTTP response conversion.
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde_json::json;
use thiserror::Error;
/// All errors that can be returned by the server.
///
/// Each variant maps to a specific HTTP status code when converted to a
/// response via [`IntoResponse`].
#[derive(Debug, Error)]
pub enum AppError {
/// The request is missing a valid `Authorization: Bearer` header, or the
/// JWT is expired / has an invalid signature.
#[error("unauthorized")]
Unauthorized,
/// The supplied credentials (username / password) were incorrect.
#[error("invalid credentials")]
InvalidCredentials,
/// The requested username is already registered.
#[error("username already taken")]
UsernameTaken,
/// The client sent a malformed or invalid request body.
#[error("bad request: {0}")]
BadRequest(String),
/// The requested resource does not exist.
#[error("not found: {0}")]
NotFound(String),
/// A database error occurred.
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
/// Password hashing failed.
#[error("internal server error")]
BcryptError(#[from] bcrypt::BcryptError),
/// JSON serialization / deserialization failed.
#[error("serialization error: {0}")]
Json(#[from] serde_json::Error),
/// A catch-all for unexpected internal failures.
#[error("internal server error")]
Internal(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::Unauthorized | AppError::InvalidCredentials => {
(StatusCode::UNAUTHORIZED, self.to_string())
}
AppError::UsernameTaken => (StatusCode::CONFLICT, self.to_string()),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
AppError::Database(e) => {
tracing::error!("database error: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".to_string(),
)
}
AppError::BcryptError(e) => {
tracing::error!("bcrypt error: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".to_string(),
)
}
AppError::Json(e) => {
tracing::error!("json error: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".to_string(),
)
}
AppError::Internal(msg) => {
tracing::error!("internal error: {msg}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".to_string(),
)
}
};
let body = Json(json!({ "error": message }));
(status, body).into_response()
}
}