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:
2026-11-12 00:39:56 +03:00
parent f9be08114d
commit 906f05f62f
41 changed files with 558 additions and 49 deletions
+11 -5
View File
@@ -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,
+10 -4
View File
@@ -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>,
+23 -4
View File
@@ -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,
}),
)
}
}
+7 -3
View File
@@ -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,