feat(hane): davet kodu ile üye katılımı ve hane izolasyonu

- AuthUser her istekte kullanıcıyı DB'den doğrular (silinen kullanıcı → 401)
- require_owner: yalnızca hane sahibine açık işlemler
- Davet kodu: oluştur, listele, iptal et (7 gün, tek kullanım)
- Davet koduyla kayıt → mevcut haneye member olarak katılma (transaction)
- GET /api/household: hane bilgisi ve üyeler
- api.http güncellendi

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 23:43:16 +03:00
parent d6136959b1
commit 4cc4fa5882
14 changed files with 497 additions and 66 deletions
+138
View File
@@ -0,0 +1,138 @@
//! 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 crate::{
auth::AuthUser,
db::col,
error::{AppError, AppResult},
models::{common::scope, household::Household, invite::Invite, user::Role},
services::invite,
state::AppState,
};
#[derive(Serialize)]
pub struct HouseholdResponse {
id: String,
name: String,
members: Vec<MemberDto>,
}
#[derive(Serialize)]
struct MemberDto {
id: String,
name: String,
email: String,
role: Role,
}
#[derive(Serialize)]
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()
}