906f05f62f
- 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>
143 lines
3.5 KiB
Rust
143 lines
3.5 KiB
Rust
//! Hane ve davet uç noktaları:
|
||
//! - `GET /api/household` → hane bilgisi + üyeler (tüm üyeler)
|
||
//! - `GET /api/household/invites` → aktif davetler (yalnızca owner)
|
||
//! - `POST /api/household/invites` → yeni davet kodu (yalnızca owner)
|
||
//! - `DELETE /api/household/invites/{id}` → daveti iptal et (yalnızca owner)
|
||
|
||
use axum::{
|
||
Json,
|
||
extract::{Path, State},
|
||
http::StatusCode,
|
||
};
|
||
use mongodb::bson::{DateTime, Uuid, doc};
|
||
use serde::{Deserialize, Serialize};
|
||
use ts_rs::TS;
|
||
|
||
use crate::{
|
||
auth::AuthUser,
|
||
db::col,
|
||
error::{AppError, AppResult},
|
||
models::{common::scope, household::Household, invite::Invite, user::Role},
|
||
services::invite,
|
||
state::AppState,
|
||
};
|
||
|
||
#[derive(Serialize, TS)]
|
||
#[ts(export, rename = "Household")]
|
||
pub struct HouseholdResponse {
|
||
id: String,
|
||
name: String,
|
||
members: Vec<MemberDto>,
|
||
}
|
||
|
||
#[derive(Serialize, TS)]
|
||
#[ts(export, rename = "Member")]
|
||
struct MemberDto {
|
||
id: String,
|
||
name: String,
|
||
email: String,
|
||
role: Role,
|
||
}
|
||
|
||
#[derive(Serialize, TS)]
|
||
#[ts(export, rename = "Invite")]
|
||
pub struct InviteDto {
|
||
id: String,
|
||
code: String,
|
||
expires_at: String,
|
||
}
|
||
|
||
impl From<Invite> for InviteDto {
|
||
fn from(i: Invite) -> Self {
|
||
Self {
|
||
id: i.id.to_string(),
|
||
code: i.code,
|
||
expires_at: iso(i.expires_at),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Üye listesi için okunan alanlar
|
||
#[derive(Deserialize)]
|
||
struct MemberRow {
|
||
#[serde(rename = "_id")]
|
||
id: Uuid,
|
||
name: String,
|
||
email: String,
|
||
role: Role,
|
||
}
|
||
|
||
pub async fn get(
|
||
State(state): State<AppState>,
|
||
auth: AuthUser,
|
||
) -> AppResult<Json<HouseholdResponse>> {
|
||
let hid = auth.household_id;
|
||
|
||
let mut hh_filter = scope(hid);
|
||
hh_filter.insert("_id", hid);
|
||
let household = state
|
||
.db
|
||
.collection::<Household>(col::HOUSEHOLDS)
|
||
.find_one(hh_filter)
|
||
.await?
|
||
.ok_or(AppError::NotFound)?;
|
||
|
||
let mut cursor = state
|
||
.db
|
||
.collection::<MemberRow>(col::USERS)
|
||
.find(scope(hid))
|
||
.projection(doc! { "name": 1, "email": 1, "role": 1 })
|
||
.sort(doc! { "created_at": 1 })
|
||
.await?;
|
||
|
||
let mut members = Vec::new();
|
||
while cursor.advance().await? {
|
||
let m = cursor.deserialize_current()?;
|
||
members.push(MemberDto {
|
||
id: m.id.to_string(),
|
||
name: m.name,
|
||
email: m.email,
|
||
role: m.role,
|
||
});
|
||
}
|
||
|
||
Ok(Json(HouseholdResponse {
|
||
id: hid.to_string(),
|
||
name: household.name,
|
||
members,
|
||
}))
|
||
}
|
||
|
||
pub async fn list_invites(
|
||
State(state): State<AppState>,
|
||
auth: AuthUser,
|
||
) -> AppResult<Json<Vec<InviteDto>>> {
|
||
auth.require_owner()?;
|
||
let list = invite::list_active(&state.db, auth.household_id).await?;
|
||
Ok(Json(list.into_iter().map(InviteDto::from).collect()))
|
||
}
|
||
|
||
pub async fn create_invite(
|
||
State(state): State<AppState>,
|
||
auth: AuthUser,
|
||
) -> AppResult<(StatusCode, Json<InviteDto>)> {
|
||
auth.require_owner()?;
|
||
let inv = invite::create(&state.db, auth.household_id, auth.user_id).await?;
|
||
Ok((StatusCode::CREATED, Json(inv.into())))
|
||
}
|
||
|
||
pub async fn revoke_invite(
|
||
State(state): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
) -> AppResult<StatusCode> {
|
||
auth.require_owner()?;
|
||
let id = Uuid::parse_str(&id).map_err(|_| AppError::NotFound)?;
|
||
invite::revoke(&state.db, auth.household_id, id).await?;
|
||
Ok(StatusCode::NO_CONTENT)
|
||
}
|
||
|
||
fn iso(dt: DateTime) -> String {
|
||
dt.try_to_rfc3339_string().unwrap_or_default()
|
||
}
|