e188922c0f
- REGISTRATION ortam değişkeni: invite_only (varsayılan) | open - invite_only: ilk hane serbest, sonrasında yalnızca davet koduyla katılım - Backend'de zorunlu: 403 registration_closed; GET /api/auth/registration - Kayıt ekranı: kayıt kapalıyken yalnızca davet kodu ve açıklama - Entegrasyon testleri: kayıt politikası (2) - Prettier ayarı eklendi (proje stiliyle uyumlu), kaynaklar biçimlendirildi Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.5 KiB
Rust
107 lines
3.5 KiB
Rust
//! 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<validator::ValidationErrors> 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<Json<T>>`
|
||
pub type AppResult<T> = Result<T, AppError>;
|