add initial crates and apps

This commit is contained in:
Evan Carroll 2026-01-12 15:34:40 -06:00
parent 5c87ba3519
commit 1ca300098f
113 changed files with 28169 additions and 0 deletions

View file

@ -0,0 +1,106 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Application error types for chattyness.
///
/// All errors derive From for automatic conversion where applicable.
#[derive(Error, Debug)]
pub enum AppError {
#[cfg(feature = "ssr")]
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[cfg(not(feature = "ssr"))]
#[error("Database error: {0}")]
Database(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Authentication required")]
Unauthorized,
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Account suspended or banned")]
AccountSuspended,
#[error("Not a staff member")]
NotStaffMember,
#[error("Password reset required")]
PasswordResetRequired,
}
/// API error response for JSON serialization.
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
impl From<AppError> for ErrorResponse {
fn from(err: AppError) -> Self {
let code = match &err {
AppError::Database(_) => Some("DATABASE_ERROR".to_string()),
AppError::Validation(_) => Some("VALIDATION_ERROR".to_string()),
AppError::Unauthorized => Some("UNAUTHORIZED".to_string()),
AppError::Forbidden(_) => Some("FORBIDDEN".to_string()),
AppError::NotFound(_) => Some("NOT_FOUND".to_string()),
AppError::Conflict(_) => Some("CONFLICT".to_string()),
AppError::Internal(_) => Some("INTERNAL_ERROR".to_string()),
AppError::InvalidCredentials => Some("INVALID_CREDENTIALS".to_string()),
AppError::AccountSuspended => Some("ACCOUNT_SUSPENDED".to_string()),
AppError::NotStaffMember => Some("NOT_STAFF_MEMBER".to_string()),
AppError::PasswordResetRequired => Some("PASSWORD_RESET_REQUIRED".to_string()),
};
ErrorResponse {
error: err.to_string(),
code,
}
}
}
#[cfg(feature = "ssr")]
mod ssr_impl {
use super::*;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = match &self {
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Validation(_) => StatusCode::BAD_REQUEST,
AppError::Unauthorized => StatusCode::UNAUTHORIZED,
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
AppError::NotFound(_) => StatusCode::NOT_FOUND,
AppError::Conflict(_) => StatusCode::CONFLICT,
AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::InvalidCredentials => StatusCode::UNAUTHORIZED,
AppError::AccountSuspended => StatusCode::FORBIDDEN,
AppError::NotStaffMember => StatusCode::FORBIDDEN,
AppError::PasswordResetRequired => StatusCode::FORBIDDEN,
};
let body = ErrorResponse::from(self);
(status, Json(body)).into_response()
}
}
}