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:
6
crates/adapters/sqlite/migrations/0002_users.sql
Normal file
6
crates/adapters/sqlite/migrations/0002_users.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
@@ -13,9 +13,12 @@ use sqlx::SqlitePool;
|
||||
|
||||
mod migrations;
|
||||
mod models;
|
||||
mod users;
|
||||
|
||||
use models::{DiaryRow, MovieRow, ReviewRow, datetime_to_str};
|
||||
|
||||
pub use users::SqliteUserRepository;
|
||||
|
||||
pub struct SqliteMovieRepository {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
76
crates/adapters/sqlite/src/users.rs
Normal file
76
crates/adapters/sqlite/src/users.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::User,
|
||||
ports::UserRepository,
|
||||
value_objects::{Email, PasswordHash, UserId},
|
||||
};
|
||||
|
||||
pub struct SqliteUserRepository {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl SqliteUserRepository {
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for SqliteUserRepository {
|
||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
||||
let email_str = email.value();
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, email, password_hash FROM users WHERE email = ?",
|
||||
email_str
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
match row {
|
||||
None => Ok(None),
|
||||
Some(r) => {
|
||||
let id = uuid::Uuid::parse_str(&r.id)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let email = Email::new(r.email)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let hash = PasswordHash::new(r.password_hash)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
Ok(Some(User::from_persistence(UserId::from_uuid(id), email, hash)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||
let id = user.id().value().to_string();
|
||||
let email = user.email().value();
|
||||
let hash = user.password_hash().value();
|
||||
let created_at = Utc::now().to_rfc3339();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"INSERT OR IGNORE INTO users (id, email, password_hash, created_at) VALUES (?, ?, ?, ?)",
|
||||
id,
|
||||
email,
|
||||
hash,
|
||||
created_at
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::ValidationError("Email already registered".into()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user