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,94 @@
|
||||
//! Hesap işlemleri: kayıt (kullanıcı + hane + başlangıç verileri).
|
||||
|
||||
use mongodb::{
|
||||
Database,
|
||||
bson::{Bson, Document, Uuid, doc},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{col, is_duplicate_key},
|
||||
error::{AppError, AppResult},
|
||||
models::{
|
||||
common::{new_id, new_meta},
|
||||
household::default_settings,
|
||||
user::Role,
|
||||
},
|
||||
services::seed,
|
||||
};
|
||||
|
||||
pub struct NewAccount {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
pub household_name: String,
|
||||
}
|
||||
|
||||
pub struct CreatedAccount {
|
||||
pub user_id: Uuid,
|
||||
pub household_id: Uuid,
|
||||
}
|
||||
|
||||
/// Kullanıcıyı, hanesini, örnek kategori ve ödeme türlerini **tek transaction** içinde oluşturur.
|
||||
/// Herhangi bir adım başarısız olursa hiçbir kayıt oluşmaz.
|
||||
pub async fn register(db: &Database, acc: NewAccount) -> AppResult<CreatedAccount> {
|
||||
let household_id = new_id();
|
||||
let user_id = new_id();
|
||||
|
||||
// Hanede household_id = _id (DATA_MODEL §3.2)
|
||||
let mut household = new_meta(household_id);
|
||||
household.insert("_id", household_id);
|
||||
household.insert("name", acc.household_name);
|
||||
household.insert("owner_id", user_id);
|
||||
household.insert("settings", default_settings());
|
||||
|
||||
let mut user = new_meta(household_id);
|
||||
user.insert("_id", user_id);
|
||||
user.insert("email", acc.email);
|
||||
user.insert("password_hash", acc.password_hash);
|
||||
user.insert("name", acc.name);
|
||||
user.insert("role", Role::Owner.as_str());
|
||||
user.insert("preferences", doc! { "theme": "system" });
|
||||
user.insert("last_login_at", Bson::Null);
|
||||
|
||||
let mut session = db.client().start_session().await?;
|
||||
session.start_transaction().await?;
|
||||
|
||||
let result: mongodb::error::Result<()> = async {
|
||||
let docs = |name| db.collection::<Document>(name);
|
||||
docs(col::HOUSEHOLDS)
|
||||
.insert_one(household)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
docs(col::USERS)
|
||||
.insert_one(user)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
docs(col::CATEGORIES)
|
||||
.insert_many(seed::categories(household_id))
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
docs(col::PAYMENT_METHODS)
|
||||
.insert_many(seed::payment_methods(household_id))
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(()) => session.commit_transaction().await?,
|
||||
Err(e) => {
|
||||
let _ = session.abort_transaction().await;
|
||||
return Err(if is_duplicate_key(&e) {
|
||||
AppError::Conflict("Bu e-posta adresi zaten kayıtlı".into())
|
||||
} else {
|
||||
e.into()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreatedAccount {
|
||||
user_id,
|
||||
household_id,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! İş mantığı. Route'lar ince kalır; asıl işi buradaki fonksiyonlar yapar.
|
||||
|
||||
pub mod account;
|
||||
pub mod seed;
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Yeni hane için örnek alt kategoriler ve ödeme türleri (DATA_MODEL.md §3.5, §3.6).
|
||||
|
||||
use mongodb::bson::{Document, Uuid};
|
||||
|
||||
use crate::models::common::new_meta;
|
||||
|
||||
/// (grup, ad, emoji)
|
||||
const CATEGORIES: &[(&str, &str, &str)] = &[
|
||||
("needs", "Market", "🛒"),
|
||||
("needs", "Kira", "🏠"),
|
||||
("needs", "Faturalar", "💡"),
|
||||
("needs", "Ulaşım", "🚌"),
|
||||
("wants", "Dışarıda yemek", "🍽️"),
|
||||
("wants", "Giyim", "👕"),
|
||||
("wants", "Eğlence", "🎉"),
|
||||
("culture", "Kitap", "📖"),
|
||||
("culture", "Eğitim", "🎓"),
|
||||
("culture", "Sinema/Konser", "🎬"),
|
||||
("unexpected", "Sağlık", "💊"),
|
||||
("unexpected", "Tamir", "🔧"),
|
||||
("unexpected", "Hediye", "🎁"),
|
||||
];
|
||||
|
||||
/// (ad, emoji)
|
||||
const PAYMENT_METHODS: &[(&str, &str)] = &[
|
||||
("Nakit", "💵"),
|
||||
("Kredi Kartı", "💳"),
|
||||
("Banka Kartı", "🏧"),
|
||||
("Havale/EFT", "🔁"),
|
||||
];
|
||||
|
||||
pub fn categories(household_id: Uuid) -> Vec<Document> {
|
||||
CATEGORIES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (group, name, emoji))| {
|
||||
let mut d = new_meta(household_id);
|
||||
d.insert("group", *group);
|
||||
d.insert("name", *name);
|
||||
d.insert("emoji", *emoji);
|
||||
d.insert("sort_order", (i as i32 + 1) * 10);
|
||||
d.insert("is_active", true);
|
||||
d
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn payment_methods(household_id: Uuid) -> Vec<Document> {
|
||||
PAYMENT_METHODS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, emoji))| {
|
||||
let mut d = new_meta(household_id);
|
||||
d.insert("name", *name);
|
||||
d.insert("emoji", *emoji);
|
||||
d.insert("sort_order", (i as i32 + 1) * 10);
|
||||
d.insert("is_active", true);
|
||||
d
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::common::new_id;
|
||||
|
||||
#[test]
|
||||
fn every_group_has_categories() {
|
||||
let docs = categories(new_id());
|
||||
for g in ["needs", "wants", "culture", "unexpected"] {
|
||||
assert!(docs.iter().any(|d| d.get_str("group").unwrap() == g));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user