Detach authentication
This commit is contained in:
@ -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_
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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(()) => (),
|
||||
|
@ -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};
|
||||
|
9
3/coursework/src/service/src/search.rs
Normal file
9
3/coursework/src/service/src/search.rs
Normal 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::*;
|
43
3/coursework/src/service/src/search/adapter.rs
Normal file
43
3/coursework/src/service/src/search/adapter.rs
Normal 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)
|
||||
}
|
||||
}
|
60
3/coursework/src/service/src/search/contract.rs
Normal file
60
3/coursework/src/service/src/search/contract.rs
Normal 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)?))
|
||||
}
|
||||
}
|
6
3/coursework/src/service/src/search/repository.rs
Normal file
6
3/coursework/src/service/src/search/repository.rs
Normal 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;
|
||||
}
|
27
3/coursework/src/service/src/search/service.rs
Normal file
27
3/coursework/src/service/src/search/service.rs
Normal 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
|
||||
}
|
||||
}
|
83
3/coursework/src/src/authentication.rs
Normal file
83
3/coursework/src/src/authentication.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
@ -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(),
|
||||
}
|
@ -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> {
|
||||
|
Reference in New Issue
Block a user