add auth system: users, login, JWT, protected routes
Domain: User entity, AuthPort/PasswordHashPort/SecretStore ports. Adapters: auth (argon2 hashing, JWT tokens), secret-store (env-based), config-sqlite user repository, http-api auth routes + extractors. Application: auth_service. SPA: login page, auth client, protected router.
This commit is contained in:
79
crates/application/src/auth_service.rs
Normal file
79
crates/application/src/auth_service.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use domain::{AuthPort, ConfigRepository, PasswordHashPort, User};
|
||||
|
||||
pub enum AuthError<E> {
|
||||
InvalidCredentials,
|
||||
RegistrationClosed,
|
||||
Repository(E),
|
||||
Hash(String),
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug> std::fmt::Display for AuthError<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidCredentials => write!(f, "invalid credentials"),
|
||||
Self::RegistrationClosed => write!(f, "registration closed (users already exist)"),
|
||||
Self::Repository(e) => write!(f, "repository error: {e:?}"),
|
||||
Self::Hash(e) => write!(f, "hash error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login<C, A, H>(
|
||||
config: &C,
|
||||
auth: &A,
|
||||
hasher: &H,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String, AuthError<C::Error>>
|
||||
where
|
||||
C: ConfigRepository,
|
||||
A: AuthPort,
|
||||
H: PasswordHashPort,
|
||||
{
|
||||
let user = config
|
||||
.get_user_by_username(username)
|
||||
.await
|
||||
.map_err(AuthError::Repository)?
|
||||
.ok_or(AuthError::InvalidCredentials)?;
|
||||
|
||||
let valid = hasher
|
||||
.verify(password, &user.password_hash)
|
||||
.await
|
||||
.map_err(AuthError::Hash)?;
|
||||
|
||||
if !valid {
|
||||
return Err(AuthError::InvalidCredentials);
|
||||
}
|
||||
|
||||
Ok(auth.generate_token(user.id))
|
||||
}
|
||||
|
||||
pub async fn register<C, H>(
|
||||
config: &C,
|
||||
hasher: &H,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), AuthError<C::Error>>
|
||||
where
|
||||
C: ConfigRepository,
|
||||
H: PasswordHashPort,
|
||||
{
|
||||
let count = config.count_users().await.map_err(AuthError::Repository)?;
|
||||
if count > 0 {
|
||||
return Err(AuthError::RegistrationClosed);
|
||||
}
|
||||
|
||||
let hash = hasher.hash(password).await.map_err(AuthError::Hash)?;
|
||||
|
||||
let user = User {
|
||||
id: 0,
|
||||
username: username.to_string(),
|
||||
password_hash: hash,
|
||||
};
|
||||
|
||||
config
|
||||
.save_user(&user)
|
||||
.await
|
||||
.map_err(AuthError::Repository)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod auth_service;
|
||||
mod config_service;
|
||||
mod data_projection;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use domain::{
|
||||
ConfigRepository, DataSource, DataSourceId, DomainEvent, EventPublisher, Layout, LayoutPreset,
|
||||
LayoutPresetId, WidgetConfig, WidgetId,
|
||||
LayoutPresetId, User, WidgetConfig, WidgetId,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
@@ -112,6 +112,18 @@ impl ConfigRepository for InMemoryConfigRepository {
|
||||
self.presets.lock().unwrap().remove(&id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_by_username(&self, _username: &str) -> Result<Option<User>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn save_user(&self, _user: &User) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_users(&self) -> Result<u32, Self::Error> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InMemoryEventPublisher {
|
||||
|
||||
Reference in New Issue
Block a user