register users
This commit is contained in:
@@ -33,12 +33,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
||||
|
||||
let warren_service = domain::warren::service::warren::Service::new(
|
||||
postgres,
|
||||
postgres.clone(),
|
||||
metrics,
|
||||
notifier,
|
||||
fs_service.clone(),
|
||||
);
|
||||
|
||||
let auth_service = domain::warren::service::auth::Service::new(postgres, metrics, notifier);
|
||||
|
||||
let server_config = HttpServerConfig::new(
|
||||
&config.server_address,
|
||||
&config.server_port,
|
||||
@@ -46,7 +48,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
config.static_frontend_dir.as_deref(),
|
||||
);
|
||||
|
||||
let http_server = HttpServer::new(warren_service, server_config).await?;
|
||||
let http_server = HttpServer::new(warren_service, auth_service, server_config).await?;
|
||||
http_server.run().await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod file;
|
||||
pub mod user;
|
||||
pub mod warren;
|
||||
|
||||
102
backend/src/lib/domain/warren/models/user/mod.rs
Normal file
102
backend/src/lib/domain/warren/models/user/mod.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
mod requests;
|
||||
pub use requests::*;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use derive_more::Display;
|
||||
use regex::Regex;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
id: Uuid,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
hash: String,
|
||||
admin: bool,
|
||||
updated_at: NaiveDateTime,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UserName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &UserEmail {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> bool {
|
||||
self.admin
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> &NaiveDateTime {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> &NaiveDateTime {
|
||||
&self.created_at
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid username
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserName(String);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum UserNameError {
|
||||
#[error("A username must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl UserName {
|
||||
pub fn new(raw: &str) -> Result<Self, UserNameError> {
|
||||
let trimmed = raw.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(UserNameError::Empty);
|
||||
}
|
||||
|
||||
Ok(Self(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid email
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserEmail(String);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum UserEmailError {
|
||||
#[error("A user's email must be a valid email")]
|
||||
Invalid,
|
||||
#[error("A user's email must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
static USER_EMAIL_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[^@]+@[^@]+\.[^@]+$").expect("Email regex"));
|
||||
|
||||
impl UserEmail {
|
||||
pub fn new(raw: &str) -> Result<Self, UserEmailError> {
|
||||
let trimmed = raw.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(UserEmailError::Empty);
|
||||
}
|
||||
|
||||
if !USER_EMAIL_REGEX.is_match(&trimmed) {
|
||||
return Err(UserEmailError::Invalid);
|
||||
}
|
||||
|
||||
Ok(Self(raw.to_string()))
|
||||
}
|
||||
}
|
||||
89
backend/src/lib/domain/warren/models/user/requests.rs
Normal file
89
backend/src/lib/domain/warren/models/user/requests.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{UserEmail, UserName};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct RegisterUserRequest {
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
password: UserPassword,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RegisterUserError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// A valid user password
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UserPassword(String);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum UserPasswordError {
|
||||
#[error("A user's password must not be empty")]
|
||||
Empty,
|
||||
#[error("A user's password must not start with a whitespace")]
|
||||
LeadingWhitespace,
|
||||
#[error("A user's password must not end with a whitespace")]
|
||||
TrailingWhitespace,
|
||||
#[error("A user's password must be longer")]
|
||||
TooShort,
|
||||
#[error("A user's password must be shorter")]
|
||||
TooLong,
|
||||
}
|
||||
|
||||
impl UserPassword {
|
||||
const MIN_LENGTH: usize = 12;
|
||||
const MAX_LENGTH: usize = 32;
|
||||
|
||||
pub fn new(raw: &str) -> Result<Self, UserPasswordError> {
|
||||
if raw.is_empty() {
|
||||
return Err(UserPasswordError::Empty);
|
||||
}
|
||||
|
||||
if raw.trim_start().len() != raw.len() {
|
||||
return Err(UserPasswordError::LeadingWhitespace);
|
||||
}
|
||||
|
||||
if raw.trim_end().len() != raw.len() {
|
||||
return Err(UserPasswordError::TrailingWhitespace);
|
||||
}
|
||||
|
||||
if raw.len() < Self::MIN_LENGTH {
|
||||
return Err(UserPasswordError::TooShort);
|
||||
}
|
||||
|
||||
if raw.len() > Self::MAX_LENGTH {
|
||||
return Err(UserPasswordError::TooLong);
|
||||
}
|
||||
|
||||
Ok(Self(raw.to_string()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RegisterUserRequest {
|
||||
pub fn new(name: UserName, email: UserEmail, password: UserPassword) -> Self {
|
||||
Self {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UserName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &UserEmail {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn password(&self) -> &UserPassword {
|
||||
&self.password
|
||||
}
|
||||
}
|
||||
@@ -48,3 +48,8 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_entry_rename_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_entry_rename_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_user_registration_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_registration_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::models::{
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{RegisterUserError, RegisterUserRequest, User},
|
||||
warren::{
|
||||
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, DeleteWarrenDirectoryError,
|
||||
DeleteWarrenDirectoryRequest, DeleteWarrenFileError, DeleteWarrenFileRequest,
|
||||
@@ -91,3 +92,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
request: RenameEntryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, RenameEntryError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
fn register_user(
|
||||
&self,
|
||||
request: RegisterUserRequest,
|
||||
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::domain::warren::models::{
|
||||
file::{AbsoluteFilePath, File, FilePath},
|
||||
user::User,
|
||||
warren::Warren,
|
||||
};
|
||||
|
||||
@@ -68,3 +69,7 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
new_path: &FilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::domain::warren::models::{
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{RegisterUserError, RegisterUserRequest, User},
|
||||
warren::{FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren},
|
||||
};
|
||||
|
||||
@@ -48,3 +49,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||
request: RenameEntryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, RenameEntryError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
fn register_user(
|
||||
&self,
|
||||
request: RegisterUserRequest,
|
||||
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
||||
}
|
||||
|
||||
51
backend/src/lib/domain/warren/service/auth.rs
Normal file
51
backend/src/lib/domain/warren/service/auth.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use crate::domain::warren::{
|
||||
models::user::{RegisterUserError, RegisterUserRequest, User},
|
||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, M, N>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
{
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
}
|
||||
|
||||
impl<R, M, N> Service<R, M, N>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
{
|
||||
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
metrics,
|
||||
notifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, M, N> AuthService for Service<R, M, N>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
{
|
||||
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
||||
let result = self.repository.register_user(request).await;
|
||||
|
||||
if let Ok(user) = result.as_ref() {
|
||||
self.metrics.record_user_registration_success().await;
|
||||
self.notifier.user_registered(user).await;
|
||||
} else {
|
||||
self.metrics.record_user_registration_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod file_system;
|
||||
pub mod warren;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
domain::warren::models::{
|
||||
file::{CreateDirectoryError, DeleteDirectoryError, DeleteFileError, ListFilesError},
|
||||
user::RegisterUserError,
|
||||
warren::{
|
||||
CreateWarrenDirectoryError, DeleteWarrenDirectoryError, DeleteWarrenFileError,
|
||||
FetchWarrenError, ListWarrenFilesError, ListWarrensError, RenameWarrenEntryError,
|
||||
@@ -116,12 +117,20 @@ impl From<ListWarrensError> for ApiError {
|
||||
|
||||
impl From<RenameWarrenEntryError> for ApiError {
|
||||
fn from(value: RenameWarrenEntryError) -> Self {
|
||||
ApiError::InternalServerError(value.to_string())
|
||||
Self::InternalServerError(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UploadWarrenFilesError> for ApiError {
|
||||
fn from(value: UploadWarrenFilesError) -> Self {
|
||||
ApiError::InternalServerError(value.to_string())
|
||||
Self::InternalServerError(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegisterUserError> for ApiError {
|
||||
fn from(value: RegisterUserError) -> Self {
|
||||
match value {
|
||||
RegisterUserError::Unknown(error) => Self::InternalServerError(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
backend/src/lib/inbound/http/handlers/auth/mod.rs
Normal file
13
backend/src/lib/inbound/http/handlers/auth/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod register;
|
||||
use register::register;
|
||||
|
||||
use axum::{Router, routing::post};
|
||||
|
||||
use crate::{
|
||||
domain::warren::ports::{AuthService, WarrenService},
|
||||
inbound::http::AppState,
|
||||
};
|
||||
|
||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||
Router::new().route("/register", post(register))
|
||||
}
|
||||
94
backend/src/lib/inbound/http/handlers/auth/register.rs
Normal file
94
backend/src/lib/inbound/http/handlers/auth/register.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::user::{
|
||||
RegisterUserRequest, UserEmail, UserEmailError, UserName, UserNameError, UserPassword,
|
||||
UserPasswordError,
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseRegisterUserHttpRequestError {
|
||||
#[error(transparent)]
|
||||
UserName(#[from] UserNameError),
|
||||
#[error(transparent)]
|
||||
UserEmail(#[from] UserEmailError),
|
||||
#[error(transparent)]
|
||||
UserPassword(#[from] UserPasswordError),
|
||||
}
|
||||
|
||||
impl From<ParseRegisterUserHttpRequestError> for ApiError {
|
||||
fn from(value: ParseRegisterUserHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseRegisterUserHttpRequestError::UserName(err) => match err {
|
||||
UserNameError::Empty => {
|
||||
Self::BadRequest("The username must not be empty".to_string())
|
||||
}
|
||||
},
|
||||
ParseRegisterUserHttpRequestError::UserEmail(err) => match err {
|
||||
UserEmailError::Invalid => Self::BadRequest("Invalid email".to_string()),
|
||||
UserEmailError::Empty => {
|
||||
Self::BadRequest("The user email must not be empty".to_string())
|
||||
}
|
||||
},
|
||||
ParseRegisterUserHttpRequestError::UserPassword(err) => match err {
|
||||
UserPasswordError::Empty => {
|
||||
Self::BadRequest("The user password must not be empty".to_string())
|
||||
}
|
||||
UserPasswordError::LeadingWhitespace => Self::BadRequest(
|
||||
"The user password must not start with a whitespace".to_string(),
|
||||
),
|
||||
UserPasswordError::TrailingWhitespace => {
|
||||
Self::BadRequest("The user password must not end with a whitespace".to_string())
|
||||
}
|
||||
UserPasswordError::TooShort => {
|
||||
Self::BadRequest("The user password must be longer".to_string())
|
||||
}
|
||||
UserPasswordError::TooLong => {
|
||||
Self::BadRequest("The user password must be shorter".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterUserHttpRequestBody {
|
||||
name: String,
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl RegisterUserHttpRequestBody {
|
||||
fn try_into_domain(self) -> Result<RegisterUserRequest, ParseRegisterUserHttpRequestError> {
|
||||
let name = UserName::new(&self.name)?;
|
||||
let email = UserEmail::new(&self.email)?;
|
||||
let password = UserPassword::new(&self.password)?;
|
||||
|
||||
Ok(RegisterUserRequest::new(name, email, password))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<RegisterUserHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.auth_service
|
||||
.register_user(domain_request)
|
||||
.await
|
||||
.map(|_| ApiSuccess::new(StatusCode::CREATED, ()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod auth;
|
||||
pub mod warrens;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::CreateWarrenDirectoryRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -62,13 +62,10 @@ impl CreateWarrenDirectoryHttpRequestBody {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_warren_directory<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn create_warren_directory<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<CreateWarrenDirectoryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::DeleteWarrenDirectoryRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -64,13 +64,10 @@ impl DeleteWarrenDirectoryHttpRequestBody {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_warren_directory<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn delete_warren_directory<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<DeleteWarrenDirectoryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::DeleteWarrenFileRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -62,13 +62,10 @@ impl DeleteWarrenFileHttpRequestBody {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_warren_file<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn delete_warren_file<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<DeleteWarrenFileHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
|
||||
@@ -6,7 +6,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::warren::{FetchWarrenRequest, Warren},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -49,8 +49,8 @@ impl FetchWarrenHttpRequestBody {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_warren<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn fetch_warren<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<FetchWarrenHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<FetchWarrenResponseData>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
file::{AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType},
|
||||
warren::ListWarrenFilesRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -94,8 +94,8 @@ impl From<Vec<File>> for ListWarrenFilesResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_warren_files<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn list_warren_files<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<ListWarrenFilesHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<ListWarrenFilesResponseData>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::warren::{ListWarrensRequest, Warren},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -41,8 +41,8 @@ impl From<&Vec<Warren>> for ListWarrensResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_warrens<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn list_warrens<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
) -> Result<ApiSuccess<ListWarrensResponseData>, ApiError> {
|
||||
let domain_request = ListWarrensRequest::new();
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ use axum::{
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
use crate::{domain::warren::ports::WarrenService, inbound::http::AppState};
|
||||
use crate::{
|
||||
domain::warren::ports::{AuthService, WarrenService},
|
||||
inbound::http::AppState,
|
||||
};
|
||||
|
||||
use fetch_warren::fetch_warren;
|
||||
use list_warren_files::list_warren_files;
|
||||
@@ -26,7 +29,7 @@ use delete_warren_file::delete_warren_file;
|
||||
use rename_warren_entry::rename_warren_entry;
|
||||
use upload_warren_files::upload_warren_files;
|
||||
|
||||
pub fn routes<WS: WarrenService>() -> Router<AppState<WS>> {
|
||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||
Router::new()
|
||||
.route("/", get(list_warrens))
|
||||
.route("/", post(fetch_warren))
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
},
|
||||
warren::RenameWarrenEntryRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -78,8 +78,8 @@ impl From<ParseRenameWarrenEntryHttpRequestError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rename_warren_entry<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn rename_warren_entry<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Json(request): Json<RenameWarrenEntryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
file::{AbsoluteFilePathError, FileName, FileNameError, FilePath, FilePathError},
|
||||
warren::{UploadFile, UploadFileList, UploadFileListError, UploadWarrenFilesRequest},
|
||||
},
|
||||
ports::WarrenService,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
@@ -92,13 +92,10 @@ impl From<ParseUploadWarrenFilesHttpRequestError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_warren_files<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
pub async fn upload_warren_files<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
TypedMultipart(multipart): TypedMultipart<UploadWarrenFilesHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = multipart.try_into_domain()?;
|
||||
|
||||
state
|
||||
|
||||
@@ -6,17 +6,18 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Router, http::HeaderValue};
|
||||
use handlers::auth;
|
||||
use handlers::warrens;
|
||||
use tokio::net::TcpListener;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{
|
||||
cors::CorsLayer,
|
||||
services::ServeDir,
|
||||
trace::{DefaultOnBodyChunk, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse},
|
||||
trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse},
|
||||
};
|
||||
use tracing::Level;
|
||||
|
||||
use crate::domain::warren::ports::WarrenService;
|
||||
use crate::domain::warren::ports::{AuthService, WarrenService};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HttpServerConfig<'a> {
|
||||
@@ -46,8 +47,9 @@ impl<'a> HttpServerConfig<'a> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState<WS: WarrenService> {
|
||||
pub struct AppState<WS: WarrenService, AS: AuthService> {
|
||||
warren_service: Arc<WS>,
|
||||
auth_service: Arc<AS>,
|
||||
}
|
||||
|
||||
pub struct HttpServer {
|
||||
@@ -56,8 +58,9 @@ pub struct HttpServer {
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub async fn new<WS: WarrenService>(
|
||||
pub async fn new<WS: WarrenService, AS: AuthService>(
|
||||
warren_service: WS,
|
||||
auth_service: AS,
|
||||
config: HttpServerConfig<'_>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let cors_layer = cors_layer(&config)?;
|
||||
@@ -73,6 +76,7 @@ impl HttpServer {
|
||||
|
||||
let state = AppState {
|
||||
warren_service: Arc::new(warren_service),
|
||||
auth_service: Arc::new(auth_service),
|
||||
};
|
||||
|
||||
let mut router = Router::new()
|
||||
@@ -118,6 +122,8 @@ fn cors_layer(config: &HttpServerConfig<'_>) -> anyhow::Result<CorsLayer> {
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
fn api_routes<WS: WarrenService>() -> Router<AppState<WS>> {
|
||||
Router::new().nest("/warrens", warrens::routes())
|
||||
fn api_routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||
Router::new()
|
||||
.nest("/auth", auth::routes())
|
||||
.nest("/warrens", warrens::routes())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::domain::warren::ports::{FileSystemMetrics, WarrenMetrics};
|
||||
use crate::domain::warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MetricsDebugLogger;
|
||||
@@ -122,3 +122,12 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] Entry rename failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthMetrics for MetricsDebugLogger {
|
||||
async fn record_user_registration_success(&self) {
|
||||
tracing::debug!("[Metrics] User registration succeeded");
|
||||
}
|
||||
async fn record_user_registration_failure(&self) {
|
||||
tracing::debug!("[Metrics] User registration failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::domain::warren::{
|
||||
models::{
|
||||
file::{File, FilePath},
|
||||
user::User,
|
||||
warren::Warren,
|
||||
},
|
||||
ports::{FileSystemNotifier, WarrenNotifier},
|
||||
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -112,3 +113,9 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
||||
tracing::debug!("[Notifier] Renamed file {} to {}", old_path, new_path);
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthNotifier for NotifierDebugLogger {
|
||||
async fn user_registered(&self, user: &User) {
|
||||
tracing::debug!("[Notifier] Registered user {}", user.name());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use argon2::{
|
||||
Argon2,
|
||||
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use sqlx::{
|
||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
@@ -8,10 +12,13 @@ use sqlx::{
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::{
|
||||
models::warren::{
|
||||
FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren,
|
||||
models::{
|
||||
user::{RegisterUserError, RegisterUserRequest, User, UserEmail, UserName, UserPassword},
|
||||
warren::{
|
||||
FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren,
|
||||
},
|
||||
},
|
||||
ports::WarrenRepository,
|
||||
ports::{AuthRepository, WarrenRepository},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -98,6 +105,52 @@ impl Postgres {
|
||||
|
||||
Ok(warrens)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
name: &UserName,
|
||||
email: &UserEmail,
|
||||
password: &UserPassword,
|
||||
is_admin: bool,
|
||||
) -> anyhow::Result<User> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2
|
||||
.hash_password(password.as_str().as_bytes(), &salt)
|
||||
.map_err(|_| anyhow!("Failed to hash password"))?
|
||||
.to_string();
|
||||
|
||||
let mut tx = connection.begin().await?;
|
||||
|
||||
let user: User = sqlx::query_as(
|
||||
"INSERT INTO users (
|
||||
name,
|
||||
email,
|
||||
hash,
|
||||
admin
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
)
|
||||
RETURNING
|
||||
*
|
||||
",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(email)
|
||||
.bind(password_hash)
|
||||
.bind(is_admin)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
impl WarrenRepository for Postgres {
|
||||
@@ -143,6 +196,29 @@ impl WarrenRepository for Postgres {
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthRepository for Postgres {
|
||||
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let user = self
|
||||
.create_user(
|
||||
&mut connection,
|
||||
request.name(),
|
||||
request.email(),
|
||||
request.password(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.context(format!("Failed to create user"))?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||
matches!(err, sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user