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
+41 -8
View File
@@ -1,15 +1,22 @@
//! Oturum cookie'si ve `AuthUser` extractor'ı.
//!
//! Bir route parametrelerine `user: AuthUser` eklediğinde, Axum isteği işlemeden önce
//! cookie'deki JWT'yi doğrular; geçersizse route hiç çalışmaz, 401 döner.
//! Bir route parametrelerine `user: AuthUser` eklediğinde, Axum isteği işlemeden önce:
//! 1. cookie'deki JWT'nin imzasını ve süresini doğrular,
//! 2. kullanıcının veritabanında hâlâ var olduğunu ve aynı haneye ait olduğunu kontrol eder
//! (silinen kullanıcının token'ı anında geçersiz olur, rol değişikliği hemen etkili olur).
//!
//! Herhangi biri başarısızsa route hiç çalışmaz, 401 döner.
//! **Hane izolasyonu:** route'lar hane kimliğini her zaman `AuthUser.household_id`'den alır,
//! asla istekten (URL/gövde) almaz.
use std::time::Duration;
use axum::{extract::FromRequestParts, http::request::Parts};
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use mongodb::bson::Uuid;
use mongodb::bson::{Uuid, doc};
use serde::Deserialize;
use crate::{error::AppError, models::user::Role, state::AppState};
use crate::{db::col, error::AppError, models::user::Role, state::AppState};
pub const COOKIE_NAME: &str = "kakeibo_session";
@@ -43,10 +50,25 @@ pub fn removal_cookie() -> Cookie<'static> {
pub struct AuthUser {
pub user_id: Uuid,
pub household_id: Uuid,
#[allow(dead_code)] // Faz 1 – Adım 4'te (davet, yetki kontrolü) kullanılacak
/// Veritabanındaki güncel rol (JWT'deki değil)
pub role: Role,
}
impl AuthUser {
/// Yalnızca hane sahibinin yapabileceği işlemler için.
pub fn require_owner(&self) -> Result<(), AppError> {
match self.role {
Role::Owner => Ok(()),
Role::Member => Err(AppError::Forbidden),
}
}
}
#[derive(Deserialize)]
struct RoleOnly {
role: Role,
}
impl FromRequestParts<AppState> for AuthUser {
type Rejection = AppError;
@@ -56,10 +78,21 @@ impl FromRequestParts<AppState> for AuthUser {
let claims = state.jwt.verify(token).ok_or(AppError::Unauthorized)?;
let parse = |s: &str| Uuid::parse_str(s).map_err(|_| AppError::Unauthorized);
let user_id = parse(&claims.sub)?;
let household_id = parse(&claims.hid)?;
let current = state
.db
.collection::<RoleOnly>(col::USERS)
.find_one(doc! { "_id": user_id, "household_id": household_id, "deleted_at": null })
.projection(doc! { "role": 1 })
.await?
.ok_or(AppError::Unauthorized)?;
Ok(Self {
user_id: parse(&claims.sub)?,
household_id: parse(&claims.hid)?,
role: claims.role,
user_id,
household_id,
role: current.role,
})
}
}