1
0

Detach authentication

This commit is contained in:
2025-02-12 09:04:19 +02:00
parent a62e8577e0
commit d01aa8d3c5
16 changed files with 278 additions and 66 deletions

View File

@ -1,4 +1,4 @@
# Stuff that helped:
# Stuff that helped
* Architecture:
- [How to apply hexagonal architecture to Rust](https://www.barrage.net/blog/technology/how-to-apply-hexagonal-architecture-to-rust)
@ -11,9 +11,10 @@
- [Building a simple text editor with iced, a cross-platform GUI library for Rust](https://www.youtube.com/watch?v=gcBJ7cPSALo)
- [Unofficial Iced Guide](https://jl710.github.io/iced-guide/)
- [icebreaker](https://github.com/hecrj/icebreaker)
- [Halloy](https://github.com/squidowl/halloy)
---
> _The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise._
— Edsger W. Dijkstra
_Edsger W. Dijkstra_

View File

@ -22,7 +22,7 @@ CREATE TABLE PackageBases (
description VARCHAR(510) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- User roles for working on packages: flagger, packager, submitter, maintainer, etc.
@ -62,7 +62,7 @@ CREATE TABLE Packages (
flagged_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (base) REFERENCES PackageBases (id) ON DELETE CASCADE
);

View File

@ -1,13 +1,13 @@
use crate::Result;
use crate::port::search::{Data, Entry, Mode, Order, SearchRepository};
use crate::{Result, adapter::mysql::search};
// use chrono::Utc;
use futures::TryStreamExt;
use sqlx::{Executor, MySql, QueryBuilder, Row};
pub struct UserAdapter;
pub struct SearchAdapter;
impl<E> SearchRepository<E> for UserAdapter
impl<E> SearchRepository<E> for SearchAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
@ -139,13 +139,13 @@ mod tests {
let data = Data {
mode: Mode::NameAndDescription,
order: Order::UpdatedAt,
search: Search::new("f")?,
search: Search::new("f").map_err(|e| e.1)?,
limit: 50,
exact: true,
ascending: false,
};
UserAdapter::search(&pool, data).await?;
SearchAdapter::search(&pool, data).await?;
Ok(())
}

View File

@ -13,10 +13,11 @@ pub use chrono::Utc;
pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter;
pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter;
pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter;
pub use adapter::mysql::search::SearchAdapter as MySqlSearchAdapter;
pub use atomic::Atomic;
pub use connect::*;
pub use port::base::{Base, BaseRepository};
pub use port::package::{Package, PackageRepository};
pub use port::search::{Search, SearchRepository};
pub use port::user::{User, UserRepository};
pub use port::*;

View File

@ -23,7 +23,7 @@ pub struct Data {
pub order: Order,
pub search: Search,
pub limit: u8,
pub limit: u16,
pub exact: bool,
pub ascending: bool,
}

View File

@ -102,7 +102,7 @@ impl TryFrom<String> for Name {
fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
struct Username<'a>(#[garde(ascii, length(chars, min = 2, max = 31))] &'a str);
struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str);
match Username(value.as_str()).validate() {
Ok(()) => (),

View File

@ -1,8 +1,8 @@
pub mod authentication;
pub mod search;
pub use authentication::{
Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository,
AuthenticationService,
};
// pub
pub use search::{Search, SearchAdapter, SearchContract, SearchRepository, SearchService};

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,43 @@
use data::search::*;
use data::{Connect, Result};
use std::marker::PhantomData;
pub struct SearchAdapter<D, C, UR>
where
C: Send,
D: Connect<Connection = C> + Sync,
UR: SearchRepository<C> + Sync,
{
driver: D,
_search_repository: PhantomData<UR>,
}
impl<D, C, UR> SearchAdapter<D, C, UR>
where
C: Send,
D: Connect<Connection = C> + Sync,
UR: SearchRepository<C> + Sync,
{
pub const fn new(driver: D) -> Self {
Self {
driver,
_search_repository: PhantomData,
}
}
}
impl<D, C, SR> super::SearchRepository for SearchAdapter<D, C, SR>
where
C: Send, //+ Sync,
D: Connect<Connection = C> + Sync,
SR: SearchRepository<C> + Sync,
{
async fn search(&self, data: Data) -> Result<Vec<Entry>> {
let c = self.driver.open_connection().await?;
let result = SR::search(&c, data).await?;
D::close_connection(c).await?;
Ok(result)
}
}

View File

@ -0,0 +1,60 @@
use data::{BoxDynError, search};
pub use data::{
Result, Validation,
search::{Mode, Order, Entry},
};
use derive_more::{Deref, Into};
use garde::Validate;
pub trait SearchContract: Send {
fn search(&self, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
}
pub struct Data {
pub mode: Mode,
pub order: Order,
pub search: Search,
pub limit: u16,
pub exact: bool,
pub ascending: bool,
}
impl From<Data> for search::Data {
fn from(value: Data) -> Self {
Self {
mode: value.mode,
order: value.order,
search: value.search.into(),
limit: value.limit,
exact: value.exact,
ascending: value.ascending,
}
}
}
pub type ReturnError<T = String> = (T, BoxDynError);
#[derive(Clone, Deref, Into)]
pub struct Search(search::Search);
impl AsRef<str> for Search {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<String> for Search {
type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)]
#[garde(transparent)]
struct Check<'a>(#[garde(ascii, length(chars, min = 1, max = 255))] &'a str);
match Check(value.as_str()).validate() {
Ok(()) => (),
Err(e) => return Err((value, e.into())),
}
Ok(Self(search::Search::new(value)?))
}
}

View File

@ -0,0 +1,6 @@
use data::Result;
use data::search::{Data, Entry};
pub trait SearchRepository {
fn search(&self, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
}

View File

@ -0,0 +1,27 @@
use super::{Data, Result, SearchContract, SearchRepository};
use data::search;
pub struct SearchService<R>
where
R: SearchRepository,
{
pub(crate) repository: R,
}
impl<R> SearchService<R>
where
R: SearchRepository,
{
pub const fn new(repository: R) -> Self {
Self { repository }
}
}
impl<R> SearchContract for SearchService<R>
where
R: SearchRepository + Send + Sync,
{
async fn search(&self, data: Data) -> Result<Vec<search::Entry>> {
self.repository.search(data.into()).await
}
}

View File

@ -0,0 +1,83 @@
mod login;
mod register;
use login::Login;
use register::Register;
use service::{Authenticated, AuthenticationContract};
use iced::{Element, Task, futures::lock::Mutex};
use std::sync::Arc;
pub struct Authentication<S> {
login: Login<S>,
register: Register<S>,
screen: Screen,
}
enum Screen {
Login,
Register,
}
#[derive(Debug)]
pub enum Message {
Login(login::Message),
Register(register::Message),
}
pub enum Event {
Task(Task<Message>),
Authenticated(Authenticated),
}
impl From<Task<Message>> for Event {
fn from(value: Task<Message>) -> Self {
Self::Task(value)
}
}
impl<S: AuthenticationContract + 'static> Authentication<S> {
pub fn new(service: Arc<Mutex<S>>) -> Self {
Self {
login: Login::new(service.clone()),
register: Register::new(service),
screen: Screen::Login,
}
}
pub fn update(&mut self, message: Message) -> Option<Event> {
Some(match message {
Message::Login(message) => match self.login.update(message)? {
login::Event::SwitchToRegister => {
self.screen = Screen::Register;
return None;
}
login::Event::Task(task) => task.map(Message::Login).into(),
login::Event::Authenticated(x) => Event::Authenticated(x),
},
Message::Register(message) => match self.register.update(message)? {
register::Event::SwitchToLogin => {
self.screen = Screen::Login;
return None;
}
register::Event::Task(task) => task.map(Message::Register).into(),
register::Event::Authenticated(x) => Event::Authenticated(x),
},
})
}
pub fn view(&self) -> Element<Message> {
match self.screen {
Screen::Login => self.login.view().map(Message::Login),
Screen::Register => self.register.view().map(Message::Register),
}
}
pub fn title(&self) -> String {
match self.screen {
Screen::Login => self.login.title(),
Screen::Register => self.register.title(),
}
}
}

View File

@ -22,6 +22,7 @@ pub struct Login<S> {
enum State {
None,
Requesting,
Success,
Error(String),
}
@ -104,7 +105,10 @@ impl<S: AuthenticationContract + 'static> Login<S> {
);
}
Message::RequestResult(r) => match &*r {
Ok(a) => return Some(Event::Authenticated(a.clone())),
Ok(a) => {
self.state = State::Success;
return Some(Event::Authenticated(a.clone()));
}
Err(e) => {
self.state = State::None;
@ -170,6 +174,7 @@ impl<S: AuthenticationContract + 'static> Login<S> {
match &self.state {
State::None => error.map_or_else(|| "Login".into(), Into::into),
State::Success => "Success".into(),
State::Requesting => "Requesting...".into(),
State::Error(e) => e.into(),
}

View File

@ -1,4 +1,4 @@
use crate::input::Input;
use crate::input::{self, Input, Value};
use crate::widget::centerbox;
use service::authentication::{self, Email, Name, Password, RegisterData};
use service::{
@ -15,7 +15,7 @@ pub struct Register<S> {
name: Input<Name>,
email: Input<Email>,
password: Input<Password>,
repeat: Input<Password>,
repeat: Input<String>,
show_password: bool,
state: State,
@ -23,6 +23,7 @@ pub struct Register<S> {
}
enum State {
None,
Success,
Requesting,
Error(String),
}
@ -72,7 +73,10 @@ impl<S: AuthenticationContract + 'static> Register<S> {
}
fn check_passwords(&mut self) {
if self.password.as_ref() != self.repeat.as_ref() {
if self.password.as_ref() == self.repeat.as_ref() {
self.repeat
.set_value(Value::Valid(self.repeat.as_ref().to_string()));
} else {
self.repeat.set_error(&"passwords are different");
}
}
@ -86,7 +90,7 @@ impl<S: AuthenticationContract + 'static> Register<S> {
self.check_passwords();
}
Message::RepeatChanged(s) => {
self.repeat.update(s);
self.repeat.set_value(Value::Valid(s));
self.check_passwords();
}
Message::ShowPasswordToggled(b) => self.show_password = b,
@ -97,10 +101,10 @@ impl<S: AuthenticationContract + 'static> Register<S> {
Message::EmailSubmitted => return Some(self.password.focus().into()),
Message::PasswordSubmitted if self.password.critical() => (),
Message::PasswordSubmitted => return Some(self.repeat.focus().into()),
Message::RepeatSubmitted if self.repeat.critical() => (),
Message::RepeatSubmitted if self.repeat.error().is_some() => (),
Message::RegisterPressed | Message::RepeatSubmitted => {
if self.repeat.critical() {
if self.repeat.error().is_some() {
return Some(self.repeat.focus().into());
}
@ -140,7 +144,10 @@ impl<S: AuthenticationContract + 'static> Register<S> {
Message::LoginPressed => return Some(Event::SwitchToLogin),
Message::RequestResult(r) => match &*r {
Ok(a) => return Some(Event::Authenticated(a.clone())),
Ok(a) => {
self.state = State::Success;
return Some(Event::Authenticated(a.clone()))
}
Err(e) => {
self.state = State::None;
@ -220,6 +227,7 @@ impl<S: AuthenticationContract + 'static> Register<S> {
match &self.state {
State::None => error.map_or_else(|| "Register".into(), Into::into),
State::Success => "Success".into(),
State::Requesting => "Requesting...".into(),
State::Error(e) => e.into(),
}

View File

@ -1,15 +1,12 @@
// mod main_window;
// mod authentication;
mod authentication;
mod input;
mod login;
mod register;
mod widget;
use std::sync::Arc;
use crate::login::Login;
use crate::register::Register;
// use crate::authentication::Authentication;
use crate::authentication::Authentication;
// use crate::main_window::MainWindow;
use data::{MySqlPool, MySqlUserAdapter, SqlxPool};
@ -40,18 +37,9 @@ fn main() -> iced::Result {
struct Repository {
scale_factor: f64,
main_id: window::Id,
login:
Login<AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>>,
register: Register<
authentication: Authentication<
AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>,
>,
screen: Screen,
// authentication: Authentication,
}
enum Screen {
Login,
Register,
}
#[derive(Debug)]
@ -61,8 +49,7 @@ enum Message {
WindowOpened(window::Id),
WindowClosed(window::Id),
Login(login::Message),
Register(register::Message),
Authentecation(authentication::Message),
// MainWindow(main_window::Message),
}
@ -87,9 +74,7 @@ impl Repository {
Self {
scale_factor: 1.4,
main_id,
login: Login::new(auth_service.clone()),
register: Register::new(auth_service),
screen: Screen::Login,
authentication: Authentication::new(auth_service),
},
Task::batch([
open_task.map(Message::WindowOpened),
@ -112,24 +97,14 @@ impl Repository {
return iced::exit();
}
}
Message::Login(message) => {
if let Some(action) = self.login.update(message) {
Message::Authentecation(message) => {
if let Some(action) = self.authentication.update(message) {
match action {
login::Event::SwitchToRegister => self.screen = Screen::Register,
login::Event::Task(task) => return task.map(Message::Login),
login::Event::Authenticated(authenticated) => {
log!("authenticated via login {:#?}", authenticated);
authentication::Event::Task(task) => {
return task.map(Message::Authentecation);
}
}
}
}
Message::Register(message) => {
if let Some(action) = self.register.update(message) {
match action {
register::Event::SwitchToLogin => self.screen = Screen::Login,
register::Event::Task(task) => return task.map(Message::Register),
register::Event::Authenticated(authenticated) => {
log!("authenticated via register: {:#?}", authenticated);
authentication::Event::Authenticated(authenticated) => {
log!("authenticated via login {:#?}", authenticated);
}
}
}
@ -145,10 +120,7 @@ impl Repository {
fn view(&self, id: window::Id) -> Element<Message> {
if self.main_id == id {
// self.main_window.view().map(Message::MainWindow)
match self.screen {
Screen::Login => self.login.view().map(Message::Login),
Screen::Register => self.register.view().map(Message::Register),
}
self.authentication.view().map(Message::Authentecation)
} else {
center(row!["This window is unknown.", "It may be closed."]).into()
}
@ -156,10 +128,7 @@ impl Repository {
fn title(&self, _: window::Id) -> String {
// "Repository".into()
match self.screen {
Screen::Login => self.login.title(),
Screen::Register => self.register.title(),
}
self.authentication.title()
}
fn subscription(&self) -> Subscription<Message> {