Domain entities/ports, service stubs, 18 application unit tests with in-memory fakes, API integration tests for /api/auth and /api/users, Vitest specs for the auth store, login page and user form, Playwright auth/user-management flow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
83 lines
2.0 KiB
Rust
83 lines
2.0 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Role {
|
|
Admin,
|
|
User,
|
|
}
|
|
|
|
impl Role {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Role::Admin => "admin",
|
|
Role::User => "user",
|
|
}
|
|
}
|
|
|
|
pub fn parse(s: &str) -> Option<Role> {
|
|
match s {
|
|
"admin" => Some(Role::Admin),
|
|
"user" => Some(Role::User),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct User {
|
|
pub id: Uuid,
|
|
pub email: String,
|
|
pub display_name: String,
|
|
pub password_hash: String,
|
|
pub role: Role,
|
|
pub is_active: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl User {
|
|
pub fn is_admin(&self) -> bool {
|
|
self.role == Role::Admin
|
|
}
|
|
}
|
|
|
|
/// Input for creating a user; the password is still in plain text here.
|
|
#[derive(Clone, Debug)]
|
|
pub struct NewUser {
|
|
pub email: String,
|
|
pub display_name: String,
|
|
pub password: String,
|
|
pub role: Role,
|
|
}
|
|
|
|
/// Partial update; `None` keeps the current value.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct UserUpdate {
|
|
pub display_name: Option<String>,
|
|
pub role: Option<Role>,
|
|
pub is_active: Option<bool>,
|
|
}
|
|
|
|
pub const MIN_PASSWORD_LEN: usize = 12;
|
|
|
|
pub fn validate_email(email: &str) -> Result<(), crate::DomainError> {
|
|
let ok = email.contains('@')
|
|
&& !email.starts_with('@')
|
|
&& !email.ends_with('@')
|
|
&& !email.contains(' ');
|
|
ok.then_some(())
|
|
.ok_or_else(|| crate::DomainError::Validation("invalid email".into()))
|
|
}
|
|
|
|
pub fn validate_password(password: &str) -> Result<(), crate::DomainError> {
|
|
(password.chars().count() >= MIN_PASSWORD_LEN)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
crate::DomainError::Validation(format!(
|
|
"password must have at least {MIN_PASSWORD_LEN} characters"
|
|
))
|
|
})
|
|
}
|