feat: JWT auth, /api prefix, SPA serving, OpenAPI, lean main.rs
- auth: register/login/refresh/logout w/ JWT+Argon2, protected mutations - domain: User, RefreshSession, auth ports, Unauthorized/Forbidden errors - presentation: context/state/factory/errors/extractors/openapi modules - routes behind /api, SPA served from root w/ fallback - OpenAPI Scalar at /docs - frontend ssr:false, single-binary Dockerfile
This commit is contained in:
18
crates/domain/src/value_objects/ids.rs
Normal file
18
crates/domain/src/value_objects/ids.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct UserId(Uuid);
|
||||
|
||||
impl UserId {
|
||||
pub fn generate() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_uuid(uuid: Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
mod chord;
|
||||
mod ids;
|
||||
mod note;
|
||||
mod sorting;
|
||||
mod user;
|
||||
|
||||
pub use chord::*;
|
||||
pub use ids::*;
|
||||
pub use note::*;
|
||||
pub use sorting::*;
|
||||
pub use user::*;
|
||||
|
||||
107
crates/domain/src/value_objects/user.rs
Normal file
107
crates/domain/src/value_objects/user.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::errors::DomainError;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn new(email: &str) -> Result<Self, DomainError> {
|
||||
let trimmed = email.trim().to_lowercase();
|
||||
if !email_address::EmailAddress::is_valid(&trimmed) {
|
||||
return Err(DomainError::ValidationError(format!(
|
||||
"invalid email: {trimmed}"
|
||||
)));
|
||||
}
|
||||
Ok(Self(trimmed))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Email {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Email({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Username(String);
|
||||
|
||||
impl Username {
|
||||
pub fn new(username: &str) -> Result<Self, DomainError> {
|
||||
let normalized = username.trim().to_lowercase();
|
||||
if normalized.len() < 2 || normalized.len() > 30 {
|
||||
return Err(DomainError::ValidationError(
|
||||
"username must be 2-30 characters".into(),
|
||||
));
|
||||
}
|
||||
if !normalized
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
return Err(DomainError::ValidationError(
|
||||
"username may only contain alphanumeric characters, underscores, and dashes".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self(normalized))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Username {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Username({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct PasswordHash(String);
|
||||
|
||||
impl PasswordHash {
|
||||
pub fn new(hash: String) -> Result<Self, DomainError> {
|
||||
if hash.is_empty() {
|
||||
return Err(DomainError::ValidationError(
|
||||
"password hash cannot be empty".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self(hash))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PasswordHash {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "PasswordHash([REDACTED])")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Password(String);
|
||||
|
||||
impl Password {
|
||||
pub fn new(password: &str) -> Result<Self, DomainError> {
|
||||
if password.len() < 8 {
|
||||
return Err(DomainError::ValidationError(
|
||||
"password must be at least 8 characters".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self(password.to_string()))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Password {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Password([REDACTED])")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user