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 98ad1b19fa
commit 1f8b24fec2
29 changed files with 599 additions and 85 deletions

View File

@@ -6,16 +6,16 @@ use domain::{
errors::DomainError,
events::DomainEvent,
models::Movie,
ports::{EventPublisher, MetadataClient, PasswordHasher, PosterFetcherClient, PosterStorage},
value_objects::{ExternalMetadataId, MovieId, PasswordHash, PosterPath, PosterUrl},
ports::{EventPublisher, MetadataClient, PosterFetcherClient, PosterStorage},
value_objects::{ExternalMetadataId, MovieId, PosterPath, PosterUrl},
};
use sqlx::SqlitePool;
use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use application::context::AppContext;
use auth::StubAuthService;
use sqlite::SqliteMovieRepository;
use application::{config::AppConfig, context::AppContext};
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
use template_askama::AskamaHtmlRenderer;
use presentation::{routes, state::AppState};
@@ -81,25 +81,9 @@ impl EventPublisher for StubEventPublisher {
}
}
struct StubPasswordHasher;
#[async_trait]
impl PasswordHasher for StubPasswordHasher {
async fn hash(&self, _plain: &str) -> Result<PasswordHash, DomainError> {
Err(DomainError::InfrastructureError(
"password hasher not implemented".into(),
))
}
async fn verify(&self, _plain: &str, _hash: &PasswordHash) -> Result<bool, DomainError> {
Err(DomainError::InfrastructureError(
"password hasher not implemented".into(),
))
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
init_tracing();
let state = wire_dependencies()
@@ -116,24 +100,32 @@ async fn main() -> anyhow::Result<()> {
}
async fn wire_dependencies() -> anyhow::Result<AppState> {
let auth_config = AuthConfig::from_env()?;
let app_config = AppConfig::from_env();
let pool = SqlitePool::connect("sqlite://reviews.db")
.await
.context("Failed to connect to SQLite database")?;
let repo = SqliteMovieRepository::new(pool);
repo.migrate()
let movie_repo = SqliteMovieRepository::new(pool.clone());
movie_repo
.migrate()
.await
.map_err(|e| anyhow::anyhow!("{}", e))
.context("Database migration failed")?;
let user_repo = SqliteUserRepository::new(pool);
let app_ctx = AppContext {
repository: Arc::new(repo),
repository: Arc::new(movie_repo),
metadata_client: Arc::new(StubMetadataClient),
poster_fetcher: Arc::new(StubPosterFetcher),
poster_storage: Arc::new(StubPosterStorage),
event_publisher: Arc::new(StubEventPublisher),
auth_service: Arc::new(StubAuthService),
password_hasher: Arc::new(StubPasswordHasher),
auth_service: Arc::new(JwtAuthService::new(auth_config)),
password_hasher: Arc::new(Argon2PasswordHasher),
user_repository: Arc::new(user_repo),
config: app_config,
};
Ok(AppState {