feat(auth): kayıt, giriş, çıkış ve oturum (argon2 + JWT httpOnly cookie)
- Kayıt: kullanıcı + hane + örnek kategori/ödeme türü tek transaction'da - Giriş: beni hatırla (30 gün / oturum), e-posta başına deneme sınırı - AuthUser extractor: cookie'deki JWT'yi doğrular - ValidJson: JSON + doğrulama, Türkçe hata mesajları - api.http: auth istekleri eklendi - Birim testleri: şifre, JWT, deneme sınırı, seed (11 test) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
//! 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.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use mongodb::bson::Uuid;
|
||||
|
||||
use crate::{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,
|
||||
#[allow(dead_code)] // Faz 1 – Adım 4'te (davet, yetki kontrolü) kullanılacak
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for AuthUser {
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, AppError> {
|
||||
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);
|
||||
Ok(Self {
|
||||
user_id: parse(&claims.sub)?,
|
||||
household_id: parse(&claims.hid)?,
|
||||
role: claims.role,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user