Files
k-tv/crates/domain/src/models/user.rs

138 lines
3.4 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::value_objects::{Email, UserId};
/// A user in the system.
///
/// Designed to be OIDC-ready: the `subject` field stores the OIDC subject claim.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
id: UserId,
subject: String,
email: Email,
password_hash: Option<String>,
is_admin: bool,
created_at: DateTime<Utc>,
}
impl User {
/// Create a new OIDC user (no local password).
pub fn new(subject: impl Into<String>, email: Email) -> Self {
Self {
id: UserId::generate(),
subject: subject.into(),
email,
password_hash: None,
is_admin: false,
created_at: Utc::now(),
}
}
/// Create a new local user with a password hash.
pub fn new_local(email: Email, password_hash: impl Into<String>) -> Self {
Self {
id: UserId::generate(),
subject: format!("local|{}", uuid::Uuid::new_v4()),
email,
password_hash: Some(password_hash.into()),
is_admin: false,
created_at: Utc::now(),
}
}
/// Hydrate from persistence — no validation, accepts all fields.
pub fn from_persistence(
id: UserId,
subject: String,
email: Email,
password_hash: Option<String>,
is_admin: bool,
created_at: DateTime<Utc>,
) -> Self {
Self {
id,
subject,
email,
password_hash,
is_admin,
created_at,
}
}
// -- Getters --
pub fn id(&self) -> UserId {
self.id
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn email(&self) -> &Email {
&self.email
}
pub fn password_hash(&self) -> Option<&str> {
self.password_hash.as_deref()
}
pub fn is_admin(&self) -> bool {
self.is_admin
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
// -- Mutations --
/// Promote this user to admin.
pub fn promote_to_admin(&mut self) {
self.is_admin = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_generates_id_and_timestamp() {
let email = Email::new("test@example.com").unwrap();
let user = User::new("oidc|123", email);
assert!(!user.is_admin());
assert!(user.password_hash().is_none());
assert_eq!(user.subject(), "oidc|123");
}
#[test]
fn new_local_sets_password_and_subject() {
let email = Email::new("local@example.com").unwrap();
let user = User::new_local(email, "hashed_pw");
assert!(user.password_hash().is_some());
assert!(user.subject().starts_with("local|"));
}
#[test]
fn from_persistence_round_trip() {
let email = Email::new("stored@example.com").unwrap();
let id = UserId::generate();
let now = Utc::now();
let user = User::from_persistence(
id,
"sub".into(),
email.clone(),
Some("hash".into()),
true,
now,
);
assert_eq!(user.id(), id);
assert_eq!(user.subject(), "sub");
assert!(user.is_admin());
assert_eq!(user.password_hash(), Some("hash"));
assert_eq!(user.created_at(), now);
}
}