138 lines
3.4 KiB
Rust
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);
|
|
}
|
|
}
|