//! Oturum cookie'si ve `AuthUser` extractor'ı. //! //! 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), //! 3. jeton şifre değişikliğinden önce verilmişse reddeder (`sessions_valid_from`). //! //! 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, doc}; use serde::Deserialize; use crate::{db::col, error::AppError, models::user::Role, state::AppState}; pub const COOKIE_NAME: &str = "kakeibo_session"; /// "Beni hatırla" seçiliyse oturum süresi pub const REMEMBER_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60); /// "Beni hatırla" seçili değilse (tarayıcı kapanınca da cookie silinir) pub const SESSION_TTL: Duration = Duration::from_secs(12 * 60 * 60); pub fn ttl(remember: bool) -> Duration { if remember { REMEMBER_TTL } else { SESSION_TTL } } pub fn session_cookie(token: String, remember: bool, secure: bool) -> Cookie<'static> { let mut c = Cookie::build((COOKIE_NAME, token)) .http_only(true) // JavaScript okuyamaz → XSS ile çalınamaz .secure(secure) // yalnızca HTTPS'te gönderilir (canlıda) .same_site(SameSite::Lax) // başka sitelerden gelen POST'larda gönderilmez .path("/"); if remember { c = c.max_age(time::Duration::seconds(REMEMBER_TTL.as_secs() as i64)); } c.build() } pub fn removal_cookie() -> Cookie<'static> { Cookie::build((COOKIE_NAME, "")).path("/").build() } /// Oturum açmış kullanıcı (JWT'den). #[derive(Debug, Clone, Copy)] pub struct AuthUser { pub user_id: Uuid, pub household_id: Uuid, /// 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, /// Şifre değişince "şimdi": bundan önce verilmiş jetonlar geçersiz #[serde(default)] sessions_valid_from: Option, } impl FromRequestParts for AuthUser { type Rejection = AppError; async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { let jar = CookieJar::from_headers(&parts.headers); let token = jar.get(COOKIE_NAME).ok_or(AppError::Unauthorized)?.value(); 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::(col::USERS) .find_one(doc! { "_id": user_id, "household_id": household_id, "deleted_at": null }) .projection(doc! { "role": 1, "sessions_valid_from": 1 }) .await? .ok_or(AppError::Unauthorized)?; if current .sessions_valid_from .is_some_and(|from| (claims.iat as i64) < from) { return Err(AppError::Unauthorized); } Ok(Self { user_id, household_id, role: current.role, }) } }