1
0

Authentication service

This commit is contained in:
2025-02-01 11:33:57 +02:00
parent ee794811c3
commit 517a81aa50
9 changed files with 462 additions and 56 deletions

View File

@ -57,6 +57,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
@ -1420,7 +1429,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f"
dependencies = [
"compact_str",
"garde_derive",
"once_cell",
"regex",
"smallvec",
"url",
]
[[package]]
name = "garde_derive"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.96",
]
[[package]]
@ -3213,6 +3238,35 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"

View File

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

View File

@ -0,0 +1,4 @@
// use database::port::user;
mod repository;
mod service;

View File

@ -0,0 +1,86 @@
pub use database::port::user::*;
use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut)]
pub struct Authenticated(User);
#[allow(async_fn_in_trait)]
pub trait AuthenticationRepository {
async fn get_user(&self, get: Get) -> Result<Option<User>>;
async fn create_user(&self, new: New) -> Result<User>;
async fn start_session(&self, user: User) -> Result<Authenticated>;
}
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),
}
}
}
// Adapter
use database::connect::Connect;
use std::marker::PhantomData;
pub struct AuthenticationAdapter<D, C, UR>
where
D: Connect<Connection = C>,
UR: UserRepository<C>,
{
driver: D,
_user_repository: PhantomData<UR>,
}
impl<D, C, UR> AuthenticationAdapter<D, C, UR>
where
D: Connect<Connection = C>,
UR: UserRepository<C>,
{
pub const fn new(driver: D) -> Self {
Self {
driver,
_user_repository: PhantomData,
}
}
}
impl<D, C, UR> AuthenticationRepository for AuthenticationAdapter<D, C, UR>
where
D: Connect<Connection = C>,
UR: UserRepository<C>,
{
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(Utc::now()))).await?;
D::close_connection(c).await?;
Ok(Authenticated(user))
}
}

View File

@ -0,0 +1,212 @@
use crate::repository::{Authenticated, AuthenticationRepository};
use database::port::user;
use derive_more::{Deref, Into};
use garde::Validate;
#[derive(thiserror::Error, Debug)]
pub enum Error {
// Login
#[error("login 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(Box<dyn std::error::Error>),
#[error("data source error: {0}")]
Repository(Box<dyn std::error::Error>),
}
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
#[derive(Clone, Deref, Into)]
pub struct Name(user::Name);
impl TryFrom<String> for Name {
type Error = Box<dyn std::error::Error>;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str);
Username(&value).validate()?;
Ok(Self(user::Name::try_from(value)?))
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(user::Email);
impl TryFrom<String> for Email {
type Error = Box<dyn std::error::Error>;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str);
Email(&value).validate()?;
Ok(Self(user::Email::try_from(value)?))
}
}
// #[derive(Validate, Deref, From, Into)]
// #[garde(transparent)]
// pub struct Password(#[garde(length(chars, min = 8))] pub String);
pub enum LoginBy {
Name(Name),
Email(Email),
}
pub struct LoginData {
login: LoginBy,
password: String,
}
pub struct RegisterData {
pub name: Name,
pub email: Email,
pub password: String,
}
pub trait AuthenticationContract {
async fn name_available(&self, name: Name) -> Result;
async fn email_available(&self, email: Email) -> Result;
async fn login(&mut self, data: LoginData) -> Result<Authenticated>;
async fn register(&mut self, data: RegisterData) -> Result<Authenticated>;
}
// Service
use crate::repository::Get;
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,
{
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,
{
async fn name_available(&self, name: Name) -> Result {
if self
.repository
.get_user(Get::Name(name.0))
.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.0))
.await
.map_err(Error::Repository)?
.is_some()
{
return Err(Error::EmailExists);
};
Ok(())
}
async fn login(&mut self, data: LoginData) -> Result<Authenticated> {
if data.password.chars().count() < 8 {
return Err(Error::InvalidPassword(
"password must be longer than 8 characters".into(),
));
}
let user = match data.login {
LoginBy::Name(name) => self.repository.get_user(Get::Name(name.0)),
LoginBy::Email(email) => self.repository.get_user(Get::Email(email.0)),
}
.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> {
if data.password.chars().count() < 8 {
return Err(Error::InvalidPassword(
"password must be longer than 8 characters".into(),
));
}
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(user::New {
name: data.name.0,
email: data.email.0,
password,
last_used: None,
})
.await
.map_err(Error::Repository)?;
self.repository
.start_session(user)
.await
.map_err(Error::Repository)
}
}

View File

@ -1,4 +1,4 @@
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
pub type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
#[allow(async_fn_in_trait)]
pub trait CRUD<C> {
@ -17,7 +17,34 @@ pub trait CRUD<C> {
async fn delete(connection: &mut C, data: Self::Unique) -> Result;
}
const TOO_LONG: &str = "too long";
trait CharLength {
fn length(&self) -> usize;
}
impl CharLength for String {
fn length(&self) -> usize {
self.chars().count()
}
}
impl CharLength for Option<String> {
fn length(&self) -> usize {
self.as_ref().map_or(0, CharLength::length)
}
}
trait MaxLength {
type Inner: CharLength;
const MAX_LENGTH: usize;
fn validate(value: &Self::Inner) -> Result<(), &'static str> {
if value.length() > Self::MAX_LENGTH {
Err("too long")
} else {
Ok(())
}
}
}
// const TOO_LONG: &str = "too long";
pub mod base;
pub mod package;

View File

@ -1,3 +1,4 @@
use super::MaxLength;
pub use super::{CRUD, Result};
pub use chrono::{DateTime, Utc};
@ -14,29 +15,30 @@ pub trait BaseRepository<C>:
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 127 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for Description {
type Error = &'static str;
fn try_from(value: Option<String>) -> std::result::Result<Self, Self::Error> {
if let Some(x) = &value {
if x.chars().count() > 510 {
return Err(super::TOO_LONG);
}
}
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}

View File

@ -1,3 +1,4 @@
use super::MaxLength;
pub use super::{CRUD, Result, base::Base};
pub use chrono::{DateTime, Utc};
@ -11,58 +12,60 @@ pub trait PackageRepository<C>:
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 127 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct Version(String);
impl MaxLength for Version {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Version {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 127 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<Option<String>> for Description {
type Error = &'static str;
fn try_from(value: Option<String>) -> std::result::Result<Self, Self::Error> {
if let Some(x) = &value {
if x.chars().count() > 255 {
return Err(super::TOO_LONG);
}
}
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct URL(Option<String>);
impl MaxLength for URL {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for URL {
type Error = &'static str;
fn try_from(value: Option<String>) -> std::result::Result<Self, Self::Error> {
if let Some(x) = &value {
if x.chars().count() > 510 {
return Err(super::TOO_LONG);
}
}
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}

View File

@ -1,3 +1,4 @@
use super::MaxLength;
pub use super::{CRUD, Result};
pub use chrono::{DateTime, Utc};
@ -11,43 +12,46 @@ pub trait UserRepository<C>:
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
type Inner = String;
const MAX_LENGTH: usize = 31;
}
impl TryFrom<String> for Name {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 31 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(String);
impl MaxLength for Email {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Email {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 255 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl MaxLength for Password {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Password {
type Error = &'static str;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
if value.chars().count() > 255 {
Err(super::TOO_LONG)
} else {
Ok(Self(value))
}
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
}
}