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:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user