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