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:
@@ -36,6 +36,9 @@ pub struct RegisterRequest {
|
||||
password: String,
|
||||
#[validate(length(max = 80, message = "Hane adı en fazla 80 karakter olabilir"))]
|
||||
household_name: Option<String>,
|
||||
/// Verilirse yeni hane kurulmaz, davet eden haneye `member` olarak katılınır.
|
||||
#[validate(length(max = 20, message = "Davet kodu geçersiz"))]
|
||||
invite_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
@@ -86,6 +89,11 @@ pub async fn register(
|
||||
.filter(|h| !h.is_empty())
|
||||
.unwrap_or_else(|| format!("{name} Hanesi"));
|
||||
|
||||
let invite_code = req
|
||||
.invite_code
|
||||
.map(|c| c.trim().to_string())
|
||||
.filter(|c| !c.is_empty());
|
||||
|
||||
let password_hash = password::hash(req.password).await?;
|
||||
let created = account::register(
|
||||
&state.db,
|
||||
@@ -94,6 +102,7 @@ pub async fn register(
|
||||
email,
|
||||
password_hash,
|
||||
household_name,
|
||||
invite_code,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -101,7 +110,7 @@ pub async fn register(
|
||||
let token = state.jwt.issue(
|
||||
created.user_id,
|
||||
created.household_id,
|
||||
Role::Owner,
|
||||
created.role,
|
||||
ttl(true),
|
||||
)?;
|
||||
let jar = jar.add(session_cookie(token, true, state.config.is_https()));
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
mod auth;
|
||||
mod health;
|
||||
mod household;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
@@ -16,7 +17,13 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/auth/register", post(auth::register))
|
||||
.route("/auth/login", post(auth::login))
|
||||
.route("/auth/logout", post(auth::logout))
|
||||
.route("/auth/me", get(auth::me));
|
||||
.route("/auth/me", get(auth::me))
|
||||
.route("/household", get(household::get))
|
||||
.route(
|
||||
"/household/invites",
|
||||
get(household::list_invites).post(household::create_invite),
|
||||
)
|
||||
.route("/household/invites/{id}", delete(household::revoke_invite));
|
||||
|
||||
Router::new().nest("/api", api).with_state(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user