feat(auth): implement JWT authentication and user registration

- Added JWT authentication with token generation and validation.
- Introduced user registration functionality with email and password.
- Integrated Argon2 for password hashing.
- Created SQLite user repository for user data persistence.
- Updated application context to include user repository and configuration settings.
- Added environment variable support for JWT secret and registration allowance.
- Enhanced error handling for unauthorized access and validation errors.
- Updated presentation layer to handle login and registration requests.
This commit is contained in:
2026-05-04 10:43:07 +02:00
parent ba42d3d445
commit 93c65cd155
29 changed files with 599 additions and 85 deletions

View File

@@ -18,3 +18,13 @@ pub struct SyncPosterCommand {
pub movie_id: Uuid,
pub external_metadata_id: String,
}
pub struct LoginCommand {
pub email: String,
pub password: String,
}
pub struct RegisterCommand {
pub email: String,
pub password: String,
}

View File

@@ -0,0 +1,13 @@
#[derive(Clone)]
pub struct AppConfig {
pub allow_registration: bool,
}
impl AppConfig {
pub fn from_env() -> Self {
let allow_registration = std::env::var("ALLOW_REGISTRATION")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
Self { allow_registration }
}
}

View File

@@ -2,9 +2,11 @@ use std::sync::Arc;
use domain::ports::{
AuthService, EventPublisher, MetadataClient, MovieRepository, PasswordHasher,
PosterFetcherClient, PosterStorage,
PosterFetcherClient, PosterStorage, UserRepository,
};
use crate::config::AppConfig;
#[derive(Clone)]
pub struct AppContext {
pub repository: Arc<dyn MovieRepository>,
@@ -14,4 +16,6 @@ pub struct AppContext {
pub event_publisher: Arc<dyn EventPublisher>,
pub auth_service: Arc<dyn AuthService>,
pub password_hasher: Arc<dyn PasswordHasher>,
pub user_repository: Arc<dyn UserRepository>,
pub config: AppConfig,
}

View File

@@ -1,4 +1,5 @@
pub mod commands;
pub mod config;
pub mod context;
pub mod ports;
pub mod queries;

View File

@@ -0,0 +1,39 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
use domain::{errors::DomainError, value_objects::Email};
use crate::{commands::LoginCommand, context::AppContext};
pub struct LoginResult {
pub token: String,
pub user_id: Uuid,
pub email: String,
pub expires_at: DateTime<Utc>,
}
pub async fn execute(ctx: &AppContext, cmd: LoginCommand) -> Result<LoginResult, DomainError> {
let email = Email::new(cmd.email)?;
let user = ctx
.user_repository
.find_by_email(&email)
.await?
.ok_or_else(|| DomainError::Unauthorized("Invalid credentials".into()))?;
let valid = ctx
.password_hasher
.verify(&cmd.password, user.password_hash())
.await?;
if !valid {
return Err(DomainError::Unauthorized("Invalid credentials".into()));
}
let generated = ctx.auth_service.generate_token(user.id()).await?;
Ok(LoginResult {
token: generated.token,
user_id: user.id().value(),
email: user.email().value().to_string(),
expires_at: generated.expires_at,
})
}

View File

@@ -1,4 +1,6 @@
pub mod get_diary;
pub mod get_review_history;
pub mod log_review;
pub mod login;
pub mod register;
pub mod sync_poster;

View File

@@ -0,0 +1,18 @@
use domain::{errors::DomainError, models::User, value_objects::Email};
use crate::{commands::RegisterCommand, context::AppContext};
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
if !ctx.config.allow_registration {
return Err(DomainError::Unauthorized("Registration is disabled".into()));
}
let email = Email::new(cmd.email)?;
if ctx.user_repository.find_by_email(&email).await?.is_some() {
return Err(DomainError::ValidationError("Email already registered".into()));
}
let hash = ctx.password_hasher.hash(&cmd.password).await?;
ctx.user_repository.save(&User::new(email, hash)).await
}