//! Uygulama genelindeki hata türü. //! Kullanıcıya Türkçe ve sade mesaj döner; teknik detay yalnızca loga yazılır. use axum::{ Json, http::StatusCode, response::{IntoResponse, Response}, }; use serde::Serialize; use ts_rs::TS; /// Tüm hata yanıtlarının gövdesi: `{ "error": { "code": "...", "message": "..." } }` #[derive(Serialize, TS)] #[ts(export)] pub struct ApiErrorBody { pub error: ApiErrorDetail, } #[derive(Serialize, TS)] #[ts(export)] pub struct ApiErrorDetail { /// Makine için: `unauthorized`, `bad_request`, `conflict`, `not_found`, ... pub code: String, /// Kullanıcıya gösterilecek Türkçe mesaj pub message: String, } #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("Kayıt bulunamadı")] NotFound, #[error("{0}")] BadRequest(String), #[error("Oturum açmanız gerekiyor")] Unauthorized, #[error("E-posta veya şifre hatalı")] InvalidCredentials, #[error("Bu işlem için yetkiniz yok")] Forbidden, #[error("Yeni hane kaydı kapalı. Katılmak için hane sahibinden davet bağlantısı isteyin.")] RegistrationClosed, #[error("{0}")] Conflict(String), #[error("Çok fazla deneme yapıldı, lütfen biraz sonra tekrar deneyin")] TooManyRequests, #[error("veritabanı hatası: {0}")] Database(#[from] mongodb::error::Error), #[error("iç hata: {0}")] Internal(String), } impl AppError { fn status_and_code(&self) -> (StatusCode, &'static str) { match self { Self::NotFound => (StatusCode::NOT_FOUND, "not_found"), Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"), Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"), Self::InvalidCredentials => (StatusCode::UNAUTHORIZED, "invalid_credentials"), Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden"), Self::RegistrationClosed => (StatusCode::FORBIDDEN, "registration_closed"), Self::Conflict(_) => (StatusCode::CONFLICT, "conflict"), Self::TooManyRequests => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests"), Self::Database(_) | Self::Internal(_) => { (StatusCode::INTERNAL_SERVER_ERROR, "internal") } } } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code) = self.status_and_code(); let message = if status.is_server_error() { // Teknik detay sadece logda; kullanıcıya genel mesaj. tracing::error!(error = %self, "sunucu hatası"); "Beklenmeyen bir hata oluştu, lütfen tekrar deneyin".to_string() } else { self.to_string() }; let body = ApiErrorBody { error: ApiErrorDetail { code: code.into(), message, }, }; (status, Json(body)).into_response() } } /// Doğrulama hatası → ilk alanın Türkçe mesajıyla 400. impl From for AppError { fn from(errors: validator::ValidationErrors) -> Self { let message = errors .field_errors() .values() .flat_map(|list| list.iter()) .find_map(|e| e.message.as_ref().map(|m| m.to_string())) .unwrap_or_else(|| "Girilen bilgiler geçersiz".into()); AppError::BadRequest(message) } } /// Route fonksiyonlarında kısa dönüş tipi: `AppResult>` pub type AppResult = Result;