feat: initialize k-tv-frontend with Next.js and Tailwind CSS
- Added package.json with dependencies and scripts for development, build, and linting. - Created postcss.config.mjs for Tailwind CSS integration. - Added SVG assets for UI components including file, globe, next, vercel, and window icons. - Configured TypeScript with tsconfig.json for strict type checking and module resolution.
This commit is contained in:
130
k-tv-backend/api/src/error.rs
Normal file
130
k-tv-backend/api/src/error.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! API error handling
|
||||
//!
|
||||
//! Maps domain errors to HTTP responses
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use domain::DomainError;
|
||||
|
||||
/// API-level errors
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ApiError {
|
||||
#[error("{0}")]
|
||||
Domain(#[from] DomainError),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("Internal server error")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("Unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
}
|
||||
|
||||
/// Error response body
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<String>,
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_response) = match &self {
|
||||
ApiError::Domain(domain_error) => {
|
||||
let status = match domain_error {
|
||||
DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_) | DomainError::NoActiveSchedule(_) => {
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
|
||||
DomainError::UserAlreadyExists(_) => StatusCode::CONFLICT,
|
||||
|
||||
DomainError::ValidationError(_) | DomainError::TimezoneError(_) => {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
// Unauthenticated = not logged in → 401
|
||||
DomainError::Unauthenticated(_) => StatusCode::UNAUTHORIZED,
|
||||
|
||||
// Forbidden = not allowed to perform action → 403
|
||||
DomainError::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||
|
||||
DomainError::RepositoryError(_) | DomainError::InfrastructureError(_) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
(
|
||||
status,
|
||||
ErrorResponse {
|
||||
error: domain_error.to_string(),
|
||||
details: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ApiError::Validation(msg) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
ErrorResponse {
|
||||
error: "Validation error".to_string(),
|
||||
details: Some(msg.clone()),
|
||||
},
|
||||
),
|
||||
|
||||
ApiError::Internal(msg) => {
|
||||
tracing::error!("Internal error: {}", msg);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorResponse {
|
||||
error: "Internal server error".to_string(),
|
||||
details: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ApiError::Forbidden(msg) => (
|
||||
StatusCode::FORBIDDEN,
|
||||
ErrorResponse {
|
||||
error: "Forbidden".to_string(),
|
||||
details: Some(msg.clone()),
|
||||
},
|
||||
),
|
||||
|
||||
ApiError::Unauthorized(msg) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
ErrorResponse {
|
||||
error: "Unauthorized".to_string(),
|
||||
details: Some(msg.clone()),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
(status, Json(error_response)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn validation(msg: impl Into<String>) -> Self {
|
||||
Self::Validation(msg.into())
|
||||
}
|
||||
|
||||
pub fn internal(msg: impl Into<String>) -> Self {
|
||||
Self::Internal(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Result type alias for API handlers
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
Reference in New Issue
Block a user