1
0

Reorganize & implement Authentication view api

This commit is contained in:
2025-02-02 22:41:32 +02:00
parent 038dec0197
commit f65312209c
69 changed files with 1089 additions and 343 deletions

13
src/service/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "service"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2.0.11"
argon2 = { version = "0.5.3", features = ["std"] }
garde = { version = "0.22.0", features = ["email", "url", "derive"] }
derive_more = { version = "1.0.0", features = ["deref", "deref_mut", "into"] }
[dependencies.data]
path = "../data"

View File

@ -0,0 +1,9 @@
pub mod adapter;
pub mod contract;
pub mod repository;
pub mod service;
pub use adapter::*;
pub use contract::*;
pub use repository::*;
pub use service::*;

View File

@ -0,0 +1,60 @@
use super::{Authenticated, AuthenticationRepository, Get};
use data::user::{Field, New, User, UserRepository};
use data::{Connect, Result};
use std::marker::PhantomData;
pub struct AuthenticationAdapter<D, C, UR>
where
C: Send,
D: Connect<Connection = C> + Sync,
UR: UserRepository<C> + Sync,
{
driver: D,
_user_repository: PhantomData<UR>,
}
impl<D, C, UR> AuthenticationAdapter<D, C, UR>
where
C: Send,
D: Connect<Connection = C> + Sync,
UR: UserRepository<C> + Sync,
{
pub const fn new(driver: D) -> Self {
Self {
driver,
_user_repository: PhantomData,
}
}
}
impl<D, C, UR> AuthenticationRepository for AuthenticationAdapter<D, C, UR>
where
C: Send,
D: Connect<Connection = C> + Sync,
UR: UserRepository<C> + Sync,
{
async fn get_user(&self, get: Get) -> Result<Option<User>> {
let c = self.driver.open_connection().await?;
let user = UR::read(&c, get.into()).await?;
D::close_connection(c).await?;
Ok(user)
}
async fn create_user(&self, new: New) -> Result<User> {
let mut c = self.driver.open_connection().await?;
let user = UR::create(&mut c, new).await?;
D::close_connection(c).await?;
Ok(user)
}
async fn start_session(&self, mut user: User) -> Result<Authenticated> {
let mut c = self.driver.open_connection().await?;
UR::update(&mut c, &mut user, Field::LastUsed(Some(data::Utc::now()))).await?;
D::close_connection(c).await?;
Ok(Authenticated(user))
}
}

View File

@ -0,0 +1,122 @@
use super::Authenticated;
use data::user;
use derive_more::{Deref, Into};
use garde::Validate;
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
pub trait AuthenticationContract {
fn name_available(&self, name: Name) -> impl Future<Output = Result> + Send;
fn email_available(&self, email: Email) -> impl Future<Output = Result> + Send;
fn login(&self, data: LoginData) -> impl Future<Output = Result<Authenticated>> + Send;
fn register(
&mut self,
data: RegisterData,
) -> impl Future<Output = Result<Authenticated>> + Send;
}
pub struct LoginData {
pub login: Login,
pub password: Password,
}
pub struct RegisterData {
pub name: Name,
pub email: Email,
pub password: Password,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
// Login
#[error("login was not found")]
LoginNotFound,
#[error("incorrect password")]
IncorrectPassword,
// Register
#[error("username is taken")]
NameExists,
#[error("email is already in use")]
EmailExists,
// Shared
#[error("invalid password: {0}")]
InvalidPassword(data::BoxDynError),
#[error("data source error: {0}")]
Repository(data::BoxDynError),
}
#[derive(Clone)]
pub enum Login {
Name(Name),
Email(Email),
}
impl TryFrom<String> for Login {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
let value = match Email::try_from(value) {
Ok(x) => return Ok(Self::Email(x)),
Err((s, _)) => s,
};
match Name::try_from(value) {
Ok(x) => Ok(Self::Name(x)),
Err((s, _)) => Err((s, "login is invalid")),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Name(user::Name);
impl TryFrom<String> for Name {
type Error = (String, Box<dyn std::error::Error>);
fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str);
if let Err(e) = Username(&value).validate() {
return Err((value, e.into()));
}
match user::Name::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(user::Email);
impl TryFrom<String> for Email {
type Error = (String, Box<dyn std::error::Error>);
fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str);
if let Err(e) = Email(&value).validate() {
return Err((value, e.into()));
}
match user::Email::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl TryFrom<String> for Password {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.chars().count() >= 8 {
Ok(Self(value))
} else {
Err((value, "password must be 8 characters or more"))
}
}
}

View File

@ -0,0 +1,26 @@
use data::Result;
use data::user::{Email, Name, New, Unique, User};
use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut, Debug)]
pub struct Authenticated(pub(super) User);
pub trait AuthenticationRepository {
fn get_user(&self, get: Get) -> impl Future<Output = Result<Option<User>>> + Send;
fn create_user(&self, new: New) -> impl Future<Output = Result<User>> + Send;
fn start_session(&self, user: User) -> impl Future<Output = Result<Authenticated>> + Send;
}
pub enum Get {
Name(Name),
Email(Email),
}
impl From<Get> for Unique {
fn from(value: Get) -> Self {
match value {
Get::Name(s) => Self::Name(s),
Get::Email(s) => Self::Email(s),
}
}
}

View File

@ -0,0 +1,114 @@
use super::{
Authenticated, AuthenticationContract, AuthenticationRepository, Email, Error, Get, Login,
LoginData, Name, RegisterData, Result,
};
use argon2::{
Argon2,
password_hash::{
self, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
},
};
impl From<password_hash::Error> for Error {
fn from(error: password_hash::Error) -> Self {
match error {
password_hash::Error::Password => Self::IncorrectPassword,
_ => Self::InvalidPassword(error.into()),
}
}
}
pub struct AuthenticationService<R>
where
R: AuthenticationRepository,
{
pub(crate) repository: R,
}
impl<R> AuthenticationService<R>
where
R: AuthenticationRepository,
{
pub const fn new(repository: R) -> Self {
Self { repository }
}
}
impl<R> AuthenticationContract for AuthenticationService<R>
where
R: AuthenticationRepository + Send + Sync,
{
async fn name_available(&self, name: Name) -> Result {
if self
.repository
.get_user(Get::Name(name.into()))
.await
.map_err(Error::Repository)?
.is_some()
{
return Err(Error::NameExists);
};
Ok(())
}
async fn email_available(&self, email: Email) -> Result {
if self
.repository
.get_user(Get::Email(email.into()))
.await
.map_err(Error::Repository)?
.is_some()
{
return Err(Error::EmailExists);
};
Ok(())
}
async fn login(&self, data: LoginData) -> Result<Authenticated> {
let user = match data.login {
Login::Name(name) => self.repository.get_user(Get::Name(name.into())),
Login::Email(email) => self.repository.get_user(Get::Email(email.into())),
}
.await
.map_err(Error::Repository)?
.ok_or(Error::LoginNotFound)?;
Argon2::default().verify_password(
data.password.as_bytes(),
&PasswordHash::new(user.password())?,
)?;
self.repository
.start_session(user)
.await
.map_err(Error::Repository)
}
async fn register(&mut self, data: RegisterData) -> Result<Authenticated> {
self.name_available(data.name.clone()).await?;
self.email_available(data.email.clone()).await?;
// Get PHC string ($argon2id$v=19$...)
let password = Argon2::default()
.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))?
.to_string()
.try_into()
.map_err(|(_, e)| Error::InvalidPassword(Box::from(e)))?;
let user = self
.repository
.create_user(data::user::New {
name: data.name.into(),
email: data.email.into(),
password,
last_used: None,
})
.await
.map_err(Error::Repository)?;
self.repository
.start_session(user)
.await
.map_err(Error::Repository)
}
}

6
src/service/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod authentication;
pub use authentication::{
Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository,
AuthenticationService,
};