feat(arayüz): API istemcisi, TanStack Query, ts-rs tip üretimi ve oturum durumu
- Backend: ts-rs ile DTO'lardan TypeScript tipleri (frontend/src/types, npm run types) - Hata ve health yanıtları tipli yapılara çevrildi - lib/api.ts: tek API istemcisi, Türkçe ApiError, ağ hatası mesajı - lib/query.ts: QueryClient (4xx tekrar denenmez, 401 → oturum kapanır) - lib/auth.ts: useMe, useLogin, useRegister, useLogout - Vitest: 7 API istemcisi testi (toplam 29) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# ts-rs: Rust DTO'larından üretilen TypeScript tipleri buraya yazılır (npm run types)
|
||||
[env]
|
||||
TS_RS_EXPORT_DIR = { value = "../frontend/src/types", relative = true }
|
||||
Generated
+32
@@ -1298,6 +1298,7 @@ dependencies = [
|
||||
"tower",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
@@ -2429,6 +2430,15 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
@@ -2656,6 +2666,28 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.22.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ time = "0.3.55"
|
||||
tokio = { version = "1.54.1", features = ["full"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
ts-rs = "12.0.1"
|
||||
uuid = { version = "1.29.2", features = ["v4", "v7"] }
|
||||
validator = { version = "0.21.0", features = ["derive"] }
|
||||
|
||||
|
||||
+25
-6
@@ -6,7 +6,24 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde_json::json;
|
||||
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 {
|
||||
@@ -59,11 +76,13 @@ impl IntoResponse for AppError {
|
||||
self.to_string()
|
||||
};
|
||||
|
||||
(
|
||||
status,
|
||||
Json(json!({ "error": { "code": code, "message": message } })),
|
||||
)
|
||||
.into_response()
|
||||
let body = ApiErrorBody {
|
||||
error: ApiErrorDetail {
|
||||
code: code.into(),
|
||||
message,
|
||||
},
|
||||
};
|
||||
(status, Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! 4 sabit ana grup (DATA_MODEL.md §2). Değiştirilemez, silinemez; veritabanında değil kodda.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum Group {
|
||||
/// 🏠 Zorunlu
|
||||
Needs,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
use mongodb::bson::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum Role {
|
||||
Owner,
|
||||
Member,
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::{Json, extract::State, http::StatusCode};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use mongodb::bson::{Uuid, doc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
@@ -26,7 +27,8 @@ use crate::{
|
||||
|
||||
// ---------- İstek / yanıt yapıları ----------
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, optional_fields)]
|
||||
pub struct RegisterRequest {
|
||||
#[validate(length(min = 1, max = 80, message = "Ad 1–80 karakter olmalı"))]
|
||||
name: String,
|
||||
@@ -41,7 +43,8 @@ pub struct RegisterRequest {
|
||||
invite_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export)]
|
||||
pub struct LoginRequest {
|
||||
#[validate(length(min = 1, message = "E-posta gerekli"))]
|
||||
email: String,
|
||||
@@ -51,13 +54,15 @@ pub struct LoginRequest {
|
||||
remember_me: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct MeResponse {
|
||||
user: UserDto,
|
||||
household: HouseholdDto,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "User")]
|
||||
struct UserDto {
|
||||
id: String,
|
||||
name: String,
|
||||
@@ -65,7 +70,8 @@ struct UserDto {
|
||||
role: Role,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "HouseholdSummary")]
|
||||
struct HouseholdDto {
|
||||
id: String,
|
||||
name: String,
|
||||
|
||||
@@ -16,6 +16,7 @@ use axum::{
|
||||
};
|
||||
use mongodb::bson::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
@@ -35,7 +36,8 @@ pub struct ListQuery {
|
||||
include_inactive: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, rename = "CatalogCreateRequest", optional_fields)]
|
||||
pub struct CreateRequest {
|
||||
group: Option<Group>,
|
||||
#[validate(length(max = 40, message = "Ad en fazla 40 karakter olabilir"))]
|
||||
@@ -44,7 +46,8 @@ pub struct CreateRequest {
|
||||
emoji: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, rename = "CatalogUpdateRequest", optional_fields)]
|
||||
pub struct UpdateRequest {
|
||||
#[validate(length(max = 40, message = "Ad en fazla 40 karakter olabilir"))]
|
||||
name: Option<String>,
|
||||
@@ -54,17 +57,20 @@ pub struct UpdateRequest {
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, rename = "CatalogOrderRequest", optional_fields)]
|
||||
pub struct OrderRequest {
|
||||
group: Option<Group>,
|
||||
#[validate(length(max = 200, message = "Liste çok uzun"))]
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "CatalogItem")]
|
||||
pub struct ItemDto {
|
||||
id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
group: Option<Group>,
|
||||
name: String,
|
||||
emoji: Option<String>,
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde_json::{Value, json};
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{db, state::AppState};
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct HealthResponse {
|
||||
/// `ok` veya `degraded`
|
||||
status: &'static str,
|
||||
/// `ok` veya `error`
|
||||
db: &'static str,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
/// Sağlık kontrolü (Dokploy health check bunu kullanır).
|
||||
/// DB erişilemezse 503 döner.
|
||||
pub async fn health(State(state): State<AppState>) -> (StatusCode, Json<Value>) {
|
||||
pub async fn health(State(state): State<AppState>) -> (StatusCode, Json<HealthResponse>) {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
match db::ping(&state.db).await {
|
||||
Ok(()) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({ "status": "ok", "db": "ok", "version": version })),
|
||||
Json(HealthResponse {
|
||||
status: "ok",
|
||||
db: "ok",
|
||||
version,
|
||||
}),
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "health: veritabanına erişilemiyor");
|
||||
(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "status": "degraded", "db": "error", "version": version })),
|
||||
Json(HealthResponse {
|
||||
status: "degraded",
|
||||
db: "error",
|
||||
version,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use axum::{
|
||||
};
|
||||
use mongodb::bson::{DateTime, Uuid, doc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
@@ -21,14 +22,16 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "Household")]
|
||||
pub struct HouseholdResponse {
|
||||
id: String,
|
||||
name: String,
|
||||
members: Vec<MemberDto>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "Member")]
|
||||
struct MemberDto {
|
||||
id: String,
|
||||
name: String,
|
||||
@@ -36,7 +39,8 @@ struct MemberDto {
|
||||
role: Role,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "Invite")]
|
||||
pub struct InviteDto {
|
||||
id: String,
|
||||
code: String,
|
||||
|
||||
Reference in New Issue
Block a user