avam-client and oauth2
This commit is contained in:
117
.vscode/launch.json
vendored
Normal file
117
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in library 'avam'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--lib",
|
||||||
|
"--package=avam"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam",
|
||||||
|
"kind": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'avam'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=avam",
|
||||||
|
"--package=avam"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'avam'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=avam",
|
||||||
|
"--package=avam"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'avam-client'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=avam-client",
|
||||||
|
"--package=avam-client"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam-client",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'avam-client'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=avam-client",
|
||||||
|
"--package=avam-client"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam-client",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in library 'avam_wasm'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--lib",
|
||||||
|
"--package=avam-wasm"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "avam_wasm",
|
||||||
|
"kind": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -3,7 +3,7 @@
|
|||||||
"leptos_macro": [
|
"leptos_macro": [
|
||||||
// optional:
|
// optional:
|
||||||
// "component",
|
// "component",
|
||||||
"server"
|
"server",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// if code that is cfg-gated for the `ssr` feature is shown as inactive,
|
// if code that is cfg-gated for the `ssr` feature is shown as inactive,
|
||||||
|
2755
Cargo.lock
generated
2755
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "avam-wasm"]
|
members = [".", "avam-client", "avam-wasm"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
@@ -31,7 +31,10 @@ ssr = [
|
|||||||
"dep:argon2",
|
"dep:argon2",
|
||||||
"dep:dotenvy",
|
"dep:dotenvy",
|
||||||
"dep:rand",
|
"dep:rand",
|
||||||
|
"dep:sha256",
|
||||||
|
"dep:jsonwebtoken",
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
|
"dep:time",
|
||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:lettre",
|
"dep:lettre",
|
||||||
@@ -41,6 +44,7 @@ ssr = [
|
|||||||
"dep:axum-macros",
|
"dep:axum-macros",
|
||||||
"dep:axum_session",
|
"dep:axum_session",
|
||||||
"dep:axum_session_sqlx",
|
"dep:axum_session_sqlx",
|
||||||
|
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:tower-layer",
|
"dep:tower-layer",
|
||||||
@@ -60,6 +64,7 @@ rand = { version = "0.8.5", optional = true }
|
|||||||
serde = { version = "1.0.210", features = ["std", "derive"], optional = false }
|
serde = { version = "1.0.210", features = ["std", "derive"], optional = false }
|
||||||
thiserror = { version = "1.0.64", optional = false }
|
thiserror = { version = "1.0.64", optional = false }
|
||||||
tokio = { version = "1.40.0", features = ["full"], optional = true }
|
tokio = { version = "1.40.0", features = ["full"], optional = true }
|
||||||
|
time = { version = "0.3.36", optional = true }
|
||||||
tracing = { version = "0.1.40", optional = false }
|
tracing = { version = "0.1.40", optional = false }
|
||||||
tracing-subscriber = { version = "0.3.18", features = [
|
tracing-subscriber = { version = "0.3.18", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
@@ -107,13 +112,19 @@ tower-layer = { version = "0.3.3", optional = true }
|
|||||||
http = "1"
|
http = "1"
|
||||||
validator = "0.18.1"
|
validator = "0.18.1"
|
||||||
|
|
||||||
|
# OAuth2
|
||||||
|
base64 = { version = "0.22.1", default-features = false }
|
||||||
|
sha256 = { version = "1.5.0", optional = true } # this fucker has a dependency on tokio?!
|
||||||
|
jsonwebtoken = { version = "9.3.0", optional = true }
|
||||||
|
serde_qs = "0.13.0"
|
||||||
|
|
||||||
[[workspace.metadata.leptos]]
|
[[workspace.metadata.leptos]]
|
||||||
name = "avam"
|
name = "avam"
|
||||||
site-root = "target/site"
|
site-root = "target/site"
|
||||||
site-pkg-dir = "pkg"
|
site-pkg-dir = "pkg"
|
||||||
style-file = "style/main.scss"
|
style-file = "style/main.scss"
|
||||||
assets-dir = "public"
|
assets-dir = "public"
|
||||||
site-addr = "0.0.0.0:3000"
|
site-addr = "192.168.1.100:3000"
|
||||||
reload-port = 3001
|
reload-port = 3001
|
||||||
browserquery = "defaults"
|
browserquery = "defaults"
|
||||||
watch = false
|
watch = false
|
||||||
|
37
avam-client/Cargo.toml
Normal file
37
avam-client/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "avam-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.40.0", features = ["full"] }
|
||||||
|
|
||||||
|
tray-icon = "0.19"
|
||||||
|
image = "0.25"
|
||||||
|
winit = "0.30"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
config = "0.14.0"
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
|
directories = "5.0"
|
||||||
|
|
||||||
|
anyhow = { version = "1.0" }
|
||||||
|
thiserror = { version = "1.0" }
|
||||||
|
winreg = "0.52.0"
|
||||||
|
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||||
|
open = "5.3.0"
|
||||||
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
|
derive_more = { version = "1.0", features = ["full"] }
|
||||||
|
uuid = { version = "1.10.0", features = ["fast-rng", "serde", "v4"] }
|
||||||
|
serde_qs = "0.13.0"
|
||||||
|
ctrlc = "3.4.5"
|
||||||
|
reqwest = { version = "0.12.8", default-features = false, features = [
|
||||||
|
"rustls-tls",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
sha256 = "1.5.0"
|
||||||
|
base64 = { version = "0.22.1", default-features = false }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
windres = "0.2"
|
5
avam-client/build.rs
Normal file
5
avam-client/build.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use windres::Build;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Build::new().compile("icon.rc").unwrap();
|
||||||
|
}
|
BIN
avam-client/icon.ico
Normal file
BIN
avam-client/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
avam-client/icon.rc
Normal file
1
avam-client/icon.rc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 ICON "icon.ico"
|
144
avam-client/src/app.rs
Normal file
144
avam-client/src/app.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
use tray_icon::menu::{MenuId, MenuItem};
|
||||||
|
use winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
event::StartCause,
|
||||||
|
event_loop::{ActiveEventLoop, ControlFlow},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{config::Config, icon::TrayIcon, oauth, state_machine::Event, BASE_URL};
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
config: Config,
|
||||||
|
tray_icon: TrayIcon,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
items: HashMap<&'static str, MenuId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(
|
||||||
|
config: Config,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let mut tray_icon = TrayIcon::new(load_icon(), crate::PROJECT_NAME);
|
||||||
|
|
||||||
|
let login = MenuItem::new("Login", true, None);
|
||||||
|
let forget = MenuItem::new("Logout and Exit", false, None);
|
||||||
|
let quit = MenuItem::new("Exit", true, None);
|
||||||
|
|
||||||
|
let mut items = HashMap::new();
|
||||||
|
|
||||||
|
let c = config.clone();
|
||||||
|
let login_id = tray_icon.add_menu_item(&login, move |item| {
|
||||||
|
// ..
|
||||||
|
let item = item.as_menuitem().unwrap();
|
||||||
|
if item.text() != "Login" {
|
||||||
|
open::that(BASE_URL)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth::open_browser(
|
||||||
|
c.code_verifier().unwrap(),
|
||||||
|
c.code_challenge_method().unwrap(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tray_icon.add_seperator()?;
|
||||||
|
|
||||||
|
let c = config.clone();
|
||||||
|
let s = sender.clone();
|
||||||
|
let forget_id = tray_icon.add_menu_item(&forget, move |_| {
|
||||||
|
c.set_token(None)?;
|
||||||
|
s.send(Event::Quit)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let s = sender.clone();
|
||||||
|
tray_icon.add_menu_item(&quit, move |_| {
|
||||||
|
s.send(Event::Quit)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
items.insert("login", login_id);
|
||||||
|
items.insert("forget", forget_id);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
tray_icon,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn new_events(&mut self, event_loop: &ActiveEventLoop, _: StartCause) {
|
||||||
|
event_loop.set_control_flow(ControlFlow::WaitUntil(
|
||||||
|
std::time::Instant::now() + std::time::Duration::from_millis(16),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Ok(event) = tray_icon::menu::MenuEvent::receiver().try_recv() {
|
||||||
|
self.tray_icon.handle(event.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(event) = self.receiver.try_recv() {
|
||||||
|
match event {
|
||||||
|
Event::Quit => {
|
||||||
|
println!("Shutting down EventLoop");
|
||||||
|
event_loop.exit()
|
||||||
|
}
|
||||||
|
Event::TokenReceived { .. } => {
|
||||||
|
self.tray_icon
|
||||||
|
.set_text(self.items.get("login").unwrap(), "Open Avam")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.tray_icon
|
||||||
|
.set_enabled(self.items.get("forget").unwrap(), true)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {
|
||||||
|
let _ = self.tray_icon.build();
|
||||||
|
|
||||||
|
let _ = self.sender.send(Event::Ready {
|
||||||
|
config: self.config.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
_: &winit::event_loop::ActiveEventLoop,
|
||||||
|
_: winit::window::WindowId,
|
||||||
|
_: winit::event::WindowEvent,
|
||||||
|
) {
|
||||||
|
// We don't have a window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon() -> tray_icon::Icon {
|
||||||
|
let icon = include_bytes!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../public/favicon-32x32.png"
|
||||||
|
));
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let image = image::load_from_memory(icon)
|
||||||
|
.expect("Failed to open icon path")
|
||||||
|
.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
|
||||||
|
}
|
125
avam-client/src/config.rs
Normal file
125
avam-client/src/config.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dirs::{Dirs, DirsError},
|
||||||
|
models::{CodeChallengeMethod, CodeVerifier},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(with = "arc_rwlock_serde")]
|
||||||
|
token: Arc<RwLock<Option<String>>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
code_verifier: Arc<RwLock<Option<CodeVerifier>>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
code_challenge_method: Arc<RwLock<Option<CodeChallengeMethod>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Config {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Dirs(#[from] DirsError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Toml(#[from] toml::ser::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn set_token(&self, token: Option<String>) -> Result<(), ConfigError> {
|
||||||
|
*self.token.write().unwrap() = token;
|
||||||
|
self.write()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> Option<String> {
|
||||||
|
self.token.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn set_code_verifier(
|
||||||
|
&self,
|
||||||
|
code_verifier: Option<CodeVerifier>,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
*self.code_verifier.write().unwrap() = code_verifier;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_code_challenge_method(
|
||||||
|
&self,
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
*self.code_challenge_method.write().unwrap() = code_challenge_method;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_verifier(&self) -> Option<CodeVerifier> {
|
||||||
|
self.code_verifier.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
||||||
|
self.code_challenge_method.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub(crate) fn new() -> Result<Self, ConfigError> {
|
||||||
|
let config = config::Config::builder()
|
||||||
|
.add_source(
|
||||||
|
config::File::with_name(Dirs::get_config_file()?.to_str().unwrap()).required(false),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config: Self = config.try_deserialize().unwrap_or_default();
|
||||||
|
|
||||||
|
config.write()?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self) -> Result<(), ConfigError> {
|
||||||
|
let toml = toml::to_string_pretty(self)?;
|
||||||
|
let mut file = std::fs::File::create(&Dirs::get_config_file()?)?;
|
||||||
|
file.write_all(toml.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod arc_rwlock_serde {
|
||||||
|
use serde::de::Deserializer;
|
||||||
|
use serde::ser::Serializer;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
pub fn serialize<S, T>(val: &Arc<RwLock<T>>, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
T::serialize(&*val.read().unwrap(), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D, T>(d: D) -> Result<Arc<RwLock<T>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
Ok(Arc::new(RwLock::new(T::deserialize(d)?)))
|
||||||
|
}
|
||||||
|
}
|
43
avam-client/src/dirs.rs
Normal file
43
avam-client/src/dirs.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub struct Dirs {}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DirsError {
|
||||||
|
#[error("Unable to get Project Directories")]
|
||||||
|
ProjectDirs,
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dirs {
|
||||||
|
pub fn get_config_dir() -> Result<PathBuf, DirsError> {
|
||||||
|
let Some(proj_dirs) = ProjectDirs::from("nl", "Avii", "Avam") else {
|
||||||
|
return Err(DirsError::ProjectDirs);
|
||||||
|
};
|
||||||
|
|
||||||
|
let config_dir = proj_dirs.config_local_dir();
|
||||||
|
|
||||||
|
if !config_dir.exists() {
|
||||||
|
std::fs::create_dir_all(config_dir)?;
|
||||||
|
}
|
||||||
|
Ok(config_dir.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_file() -> Result<PathBuf, DirsError> {
|
||||||
|
let mut c = Self::get_config_dir()?;
|
||||||
|
c.push("config.toml");
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lock_file() -> Result<PathBuf, DirsError> {
|
||||||
|
let mut c = Self::get_config_dir()?;
|
||||||
|
c.push(".lock");
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
}
|
117
avam-client/src/icon.rs
Normal file
117
avam-client/src/icon.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use tray_icon::{
|
||||||
|
menu::{IsMenuItem, Menu, MenuId, MenuItemKind, PredefinedMenuItem},
|
||||||
|
TrayIconBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The app winit thing can listen in on the same events
|
||||||
|
// coming from the state machine to give feedback to the user
|
||||||
|
// changing the icon, the alt texts, and the context menu
|
||||||
|
// based on the last received event
|
||||||
|
|
||||||
|
type Callback = Box<dyn Fn(&MenuItemKind) -> Result<(), anyhow::Error> + 'static>;
|
||||||
|
|
||||||
|
pub struct TrayIcon {
|
||||||
|
icon: tray_icon::Icon,
|
||||||
|
title: String,
|
||||||
|
menu_items: HashMap<MenuId, Callback>,
|
||||||
|
menu: Menu,
|
||||||
|
tray_icon: Option<tray_icon::TrayIcon>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl TrayIcon {
|
||||||
|
pub fn new(icon: tray_icon::Icon, title: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
icon,
|
||||||
|
title: title.to_string(),
|
||||||
|
menu_items: HashMap::new(),
|
||||||
|
menu: Menu::new(),
|
||||||
|
tray_icon: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_seperator(&mut self) -> Result<(), anyhow::Error> {
|
||||||
|
self.menu.append(&PredefinedMenuItem::separator())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_menu_item<F>(
|
||||||
|
&mut self,
|
||||||
|
item: &dyn IsMenuItem,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<MenuId, anyhow::Error>
|
||||||
|
where
|
||||||
|
F: Fn(&MenuItemKind) -> Result<(), anyhow::Error> + 'static,
|
||||||
|
{
|
||||||
|
let id = item.id().clone();
|
||||||
|
self.menu.append(item)?;
|
||||||
|
|
||||||
|
self.menu_items
|
||||||
|
.insert(id.clone(), Box::new(callback) as Callback);
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_enabled(&self, id: &MenuId, enabled: bool) -> Result<(), anyhow::Error> {
|
||||||
|
if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||||
|
if let Some(menuitem) = item.as_menuitem() {
|
||||||
|
menuitem.set_enabled(enabled);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_check_menuitem() {
|
||||||
|
menuitem.set_enabled(enabled);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_icon_menuitem() {
|
||||||
|
menuitem.set_enabled(enabled);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_submenu() {
|
||||||
|
menuitem.set_enabled(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&self, id: &MenuId, text: &str) -> Result<(), anyhow::Error> {
|
||||||
|
if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||||
|
if let Some(menuitem) = item.as_menuitem() {
|
||||||
|
menuitem.set_text(text);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_check_menuitem() {
|
||||||
|
menuitem.set_text(text);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_icon_menuitem() {
|
||||||
|
menuitem.set_text(text);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_predefined_menuitem() {
|
||||||
|
menuitem.set_text(text);
|
||||||
|
}
|
||||||
|
if let Some(menuitem) = item.as_submenu() {
|
||||||
|
menuitem.set_text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&self, id: &MenuId) {
|
||||||
|
if let Some(item) = self.menu_items.get(id) {
|
||||||
|
if let Some(i) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||||
|
if let Err(e) = item(i) {
|
||||||
|
eprintln!("{:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&mut self) -> Result<(), anyhow::Error> {
|
||||||
|
self.tray_icon = Some(
|
||||||
|
TrayIconBuilder::new()
|
||||||
|
.with_tooltip(self.title.clone())
|
||||||
|
.with_menu(Box::new(self.menu.clone()))
|
||||||
|
.with_icon(self.icon.clone())
|
||||||
|
.build()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
48
avam-client/src/lock.rs
Normal file
48
avam-client/src/lock.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::dirs::{Dirs, DirsError};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub struct Lock {
|
||||||
|
force: bool,
|
||||||
|
lock_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum LockError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Dirs(#[from] DirsError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lock {
|
||||||
|
pub fn new(force: bool) -> Result<Self, LockError> {
|
||||||
|
let lock_file = Dirs::get_lock_file()?;
|
||||||
|
|
||||||
|
Ok(Self { lock_file, force })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lock() {
|
||||||
|
if let Ok(lock_file) = Dirs::get_lock_file() {
|
||||||
|
if !fs::exists(&lock_file).unwrap() {
|
||||||
|
fs::write(&lock_file, "").expect("there to not be issues with the filesystem");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock() {
|
||||||
|
if let Ok(lock_file) = Dirs::get_lock_file() {
|
||||||
|
if fs::exists(&lock_file).unwrap() {
|
||||||
|
let _ = fs::remove_file(lock_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_locked(&self) -> bool {
|
||||||
|
!self.force
|
||||||
|
&& fs::exists(&self.lock_file).expect("there to not be issues with the filesystem")
|
||||||
|
}
|
||||||
|
}
|
180
avam-client/src/main.rs
Normal file
180
avam-client/src/main.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod config;
|
||||||
|
mod dirs;
|
||||||
|
mod icon;
|
||||||
|
mod lock;
|
||||||
|
mod models;
|
||||||
|
mod oauth;
|
||||||
|
mod pipe;
|
||||||
|
mod state_machine;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use clap::Parser;
|
||||||
|
use lock::Lock;
|
||||||
|
use oauth::{start_code_listener, start_code_to_token};
|
||||||
|
use pipe::Pipe;
|
||||||
|
use state_machine::Event;
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
|
pub static BASE_URL: &str = "https://avam.avii.nl";
|
||||||
|
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
||||||
|
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
||||||
|
pub static CLIENT_ID: uuid::Uuid = uuid::uuid!("f9525060-0a34-4233-87e2-0f9990b7c6db");
|
||||||
|
pub static REDIRECT_URI: &str = "avam:token";
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Arguments {
|
||||||
|
#[arg(short, long)]
|
||||||
|
code: Option<String>,
|
||||||
|
#[arg(short, long, action)]
|
||||||
|
force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
let (event_sender, event_receiver) = tokio::sync::broadcast::channel(1);
|
||||||
|
let args = Arguments::parse();
|
||||||
|
|
||||||
|
if handle_single_instance(&args).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
register_url_scheme()?;
|
||||||
|
|
||||||
|
let config = Config::new()?;
|
||||||
|
|
||||||
|
let mut futures = JoinSet::new();
|
||||||
|
|
||||||
|
// Register Quit handler
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let mut ctrl_c_counter = 0;
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
println!("CTRL_C: Quit singal sent");
|
||||||
|
ctrl_c_counter += 1;
|
||||||
|
if ctrl_c_counter >= 3 {
|
||||||
|
let _ = unregister_url_scheme();
|
||||||
|
Lock::unlock();
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = sender.send(Event::Quit) {
|
||||||
|
println!("{:#?}", e)
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Start the State Machine
|
||||||
|
let c = config.clone();
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
futures.spawn(state_machine::start(c, sender, receiver));
|
||||||
|
|
||||||
|
// // Start the code listener
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
let (pipe_sender, pipe_receiver) = tokio::sync::broadcast::channel(100);
|
||||||
|
futures.spawn(start_code_listener(pipe_sender, receiver));
|
||||||
|
|
||||||
|
// Start token listener
|
||||||
|
let c = config.clone();
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver));
|
||||||
|
|
||||||
|
// Start the Tray Icon
|
||||||
|
let c = config.clone();
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
start_tray_icon(c, sender, receiver).await?;
|
||||||
|
|
||||||
|
// Wait for everything to finish
|
||||||
|
while let Some(result) = futures.join_next().await {
|
||||||
|
if let Ok(Err(e)) = result {
|
||||||
|
panic!("{:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
unregister_url_scheme()?;
|
||||||
|
Lock::unlock();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
use winit::event_loop::EventLoop;
|
||||||
|
async fn start_tray_icon(
|
||||||
|
config: Config,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let mut app = App::new(config, sender, receiver)?;
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
println!("EventLoop Shutdonw");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
/// returns `Ok(true)` if we need to quit gracefully
|
||||||
|
/// returns `Err(e)` if we're already running
|
||||||
|
async fn handle_single_instance(args: &Arguments) -> Result<bool, anyhow::Error> {
|
||||||
|
let lock = Lock::new(args.force)?;
|
||||||
|
|
||||||
|
if lock.is_locked() && args.code.is_none() {
|
||||||
|
return Err(anyhow::anyhow!("Lockfile exists, exiting."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(code) = &args.code {
|
||||||
|
Pipe::send(code).await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lock::lock();
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_url_scheme() -> Result<(), anyhow::Error> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let avam_schema_root = hkcu.create_subkey("Software\\Classes\\avam")?;
|
||||||
|
avam_schema_root.0.set_value("URL Protocol", &"")?;
|
||||||
|
let command = avam_schema_root.0.create_subkey("shell\\open\\command")?;
|
||||||
|
|
||||||
|
let current_exec = std::env::current_exe()?;
|
||||||
|
|
||||||
|
command.0.set_value(
|
||||||
|
"",
|
||||||
|
&format!("\"{}\" -c \"%1\"", current_exec.to_str().unwrap()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister_url_scheme() -> Result<(), anyhow::Error> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
hkcu.delete_subkey_all("Software\\Classes\\avam").ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
300
avam-client/src/models.rs
Normal file
300
avam-client/src/models.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use derive_more::{derive::From, Display};
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::REDIRECT_URI;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Display, From, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct RedirectUri(String);
|
||||||
|
|
||||||
|
impl RedirectUri {
|
||||||
|
pub fn new(uri: &str) -> Self {
|
||||||
|
Self(uri.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for RedirectUri {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "String")]
|
||||||
|
pub struct CodeVerifier(String);
|
||||||
|
|
||||||
|
impl From<String> for CodeVerifier {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for CodeVerifier {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeVerifier {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let token: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(120)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeVerifier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CodeChallengeMethodError {
|
||||||
|
#[error("Code challenge method is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum CodeChallengeMethod {
|
||||||
|
#[serde(rename = "plain")]
|
||||||
|
Plain,
|
||||||
|
#[serde(rename = "S256")]
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CodeChallengeMethod {
|
||||||
|
type Err = CodeChallengeMethodError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"plain" => Ok(Self::Plain),
|
||||||
|
"S256" => Ok(Self::Sha256),
|
||||||
|
_ => Err(CodeChallengeMethodError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ResponseTypeError {
|
||||||
|
#[error("The response type is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum ResponseType {
|
||||||
|
#[serde(rename = "code")]
|
||||||
|
Code,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ResponseType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
ResponseType::Code => "code",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ResponseType {
|
||||||
|
type Err = ResponseTypeError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"code" => Ok(Self::Code),
|
||||||
|
_ => Err(ResponseTypeError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GrantTypeError {
|
||||||
|
#[error("The grant type is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum GrantType {
|
||||||
|
#[serde(rename = "authorization_code")]
|
||||||
|
AuthorizationCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GrantType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
GrantType::AuthorizationCode => "authorization_code",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for GrantType {
|
||||||
|
type Err = GrantTypeError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"authorization_code" => Ok(Self::AuthorizationCode),
|
||||||
|
_ => Err(GrantTypeError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizeRequest {
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
response_type: ResponseType, // Make type (enum:code,)
|
||||||
|
state: Option<String>, // random string for CSRF protection
|
||||||
|
code_challenge: String, // pkce
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>, // Make type (enum:sha256,) hashing algo
|
||||||
|
redirect_uri: RedirectUri, // Make type
|
||||||
|
scope: Option<String>, // space seperated string with permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizeRequest {
|
||||||
|
pub fn new(
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
response_type: ResponseType,
|
||||||
|
state: Option<String>,
|
||||||
|
code_challenge: String,
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
scope: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id,
|
||||||
|
response_type,
|
||||||
|
state,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
redirect_uri,
|
||||||
|
scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
// self.client_id
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn response_type(&self) -> ResponseType {
|
||||||
|
// self.response_type.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn state(&self) -> Option<String> {
|
||||||
|
// self.state.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn code_challenge(&self) -> String {
|
||||||
|
// self.code_challenge.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
||||||
|
// self.code_challenge_method.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn redirect_uri(&self) -> String {
|
||||||
|
// self.redirect_uri.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn scope(&self) -> Option<String> {
|
||||||
|
// self.scope.clone()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationResponse {
|
||||||
|
code: String,
|
||||||
|
state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for AuthorizationResponse {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
let mut parts = s.split('?');
|
||||||
|
let Some(protocol) = parts.next() else {
|
||||||
|
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if protocol != REDIRECT_URI {
|
||||||
|
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(qs) = parts.next() else {
|
||||||
|
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if parts.count() > 0 {
|
||||||
|
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_qs::from_str(qs)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizationResponse {
|
||||||
|
pub fn code(&self) -> String {
|
||||||
|
self.code.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationCodeRequest {
|
||||||
|
grant_type: GrantType,
|
||||||
|
code: String,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizationCodeRequest {
|
||||||
|
pub fn new(
|
||||||
|
grant_type: GrantType,
|
||||||
|
code: String,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
grant_type,
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
client_id,
|
||||||
|
code_verifier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationCodeResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizationCodeResponse {
|
||||||
|
pub fn token(&self) -> String {
|
||||||
|
self.token.clone()
|
||||||
|
}
|
||||||
|
}
|
95
avam-client/src/oauth.rs
Normal file
95
avam-client/src/oauth.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
sync::broadcast::{Receiver, Sender},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config, models::*, pipe::Pipe, state_machine::Event, BASE_URL, CLIENT_ID, REDIRECT_URI,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn open_browser(
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let code_challenge = match code_challenge_method {
|
||||||
|
CodeChallengeMethod::Plain => {
|
||||||
|
use base64::prelude::*;
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string())
|
||||||
|
}
|
||||||
|
CodeChallengeMethod::Sha256 => {
|
||||||
|
use base64::prelude::*;
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = AuthorizeRequest::new(
|
||||||
|
CLIENT_ID,
|
||||||
|
ResponseType::Code,
|
||||||
|
None,
|
||||||
|
code_challenge.clone(),
|
||||||
|
Some(code_challenge_method.clone()),
|
||||||
|
RedirectUri::new(REDIRECT_URI),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let qs = serde_qs::to_string(&request)?;
|
||||||
|
|
||||||
|
open::that(format!("{}/oauth2/authorize?{}", BASE_URL, qs))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_code_listener(
|
||||||
|
pipe_sender: Sender<AuthorizationResponse>,
|
||||||
|
event_receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let pipe = Pipe::new(event_receiver.resubscribe());
|
||||||
|
pipe.listen(pipe_sender).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_code_to_token(
|
||||||
|
config: Config,
|
||||||
|
mut pipe_receiver: Receiver<AuthorizationResponse>,
|
||||||
|
event_sender: Sender<Event>,
|
||||||
|
mut event_receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = sleep(Duration::from_millis(100)) => {
|
||||||
|
if let Ok(Event::Quit) = event_receiver.try_recv() {
|
||||||
|
println!("Shutting down Code Transformer");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response) = pipe_receiver.recv() => {
|
||||||
|
let r = AuthorizationCodeRequest::new(
|
||||||
|
GrantType::AuthorizationCode,
|
||||||
|
response.code(),
|
||||||
|
REDIRECT_URI.into(),
|
||||||
|
CLIENT_ID,
|
||||||
|
config.code_verifier().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let qs = serde_qs::to_string(&r)?;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(format!("{}/oauth2/token", BASE_URL))
|
||||||
|
.body(qs)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response: AuthorizationCodeResponse = response.json().await?;
|
||||||
|
|
||||||
|
event_sender.send(Event::TokenReceived { token: response.token() })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Code Transformer Shutdown");
|
||||||
|
Ok(())
|
||||||
|
}
|
97
avam-client/src/pipe.rs
Normal file
97
avam-client/src/pipe.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use interprocess::os::windows::named_pipe::{pipe_mode, tokio::*, PipeListenerOptions};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
select,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::models::AuthorizationResponse;
|
||||||
|
use crate::state_machine::Event;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PipeError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Pipe {
|
||||||
|
quit_signal: Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PIPE_NAME: &str = "AvamICP";
|
||||||
|
|
||||||
|
impl Pipe {
|
||||||
|
pub fn new(quit_signal: Receiver<Event>) -> Self {
|
||||||
|
Self { quit_signal }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(msg: &str) -> Result<(), PipeError> {
|
||||||
|
let conn = DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(format!(
|
||||||
|
r"\\.\pipe\{}",
|
||||||
|
PIPE_NAME
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
let (_, mut sender) = conn.split();
|
||||||
|
|
||||||
|
sender.write_all(msg.as_bytes()).await?;
|
||||||
|
sender.shutdown().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listen(
|
||||||
|
&self,
|
||||||
|
pipe_sender: Sender<AuthorizationResponse>,
|
||||||
|
) -> Result<(), PipeError> {
|
||||||
|
// todo: give state so we can gracefully shutdown the pipe
|
||||||
|
let mut quit_signal = self.quit_signal.resubscribe();
|
||||||
|
let listener = PipeListenerOptions::new()
|
||||||
|
.path(Path::new(&format!(r"\\.\pipe\{}", PIPE_NAME)))
|
||||||
|
.create_tokio_duplex::<pipe_mode::Bytes>()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
Ok(conn) = listener.accept() => {
|
||||||
|
let new_sender = pipe_sender.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = Self::handle_conn(conn, new_sender).await {
|
||||||
|
eprintln!("error while handling connection: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_ = sleep(Duration::from_millis(100)) => {
|
||||||
|
if let Ok(Event::Quit) = quit_signal.try_recv() {
|
||||||
|
println!("Shutting down Code Listener");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Code Listener Shutdown");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_conn(
|
||||||
|
conn: DuplexPipeStream<pipe_mode::Bytes>,
|
||||||
|
pipe_sender: Sender<AuthorizationResponse>,
|
||||||
|
) -> Result<(), PipeError> {
|
||||||
|
let (mut recver, _) = conn.split();
|
||||||
|
|
||||||
|
let mut buffer = Vec::with_capacity(512);
|
||||||
|
|
||||||
|
recver.read_buf(&mut buffer).await?;
|
||||||
|
|
||||||
|
let as_string = String::from_utf8_lossy(&buffer).to_string();
|
||||||
|
let response: AuthorizationResponse = as_string.try_into()?;
|
||||||
|
|
||||||
|
let _ = pipe_sender.send(response);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
149
avam-client/src/state_machine.rs
Normal file
149
avam-client/src/state_machine.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
models::{CodeChallengeMethod, CodeVerifier},
|
||||||
|
oauth,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
Init,
|
||||||
|
AppStart {
|
||||||
|
config: Config,
|
||||||
|
},
|
||||||
|
Authenticate {
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
},
|
||||||
|
Connect {
|
||||||
|
token: String,
|
||||||
|
},
|
||||||
|
WaitForSim,
|
||||||
|
Running,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Event {
|
||||||
|
Ready {
|
||||||
|
config: Config,
|
||||||
|
},
|
||||||
|
StartAuthenticate {
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
}, // should not be string
|
||||||
|
TokenReceived {
|
||||||
|
token: String,
|
||||||
|
}, // AppStart and Authenticate can fire off TokenReceived to transition into Connect
|
||||||
|
Connected, // Once connected to the socket, and properly authenticated, fire off Connected to transition to WaitForSim
|
||||||
|
Disconnected, // If for whatever reason we're disconnected from the backend, we need to transition back to Connect
|
||||||
|
SimConnected, // SimConnect is connected, we're in the world and ready to send data, transition to Running
|
||||||
|
SimDisconnected, // SimConnect is disconnected, we've finished the flight and exited back to the menu, transition back to WaitForSim
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub async fn next(self, event: Event) -> State {
|
||||||
|
match (self, event) {
|
||||||
|
// (Current State, SomeEvent) => NextState
|
||||||
|
(State::Init, Event::Ready { config }) => State::AppStart { config },
|
||||||
|
(
|
||||||
|
State::AppStart { .. },
|
||||||
|
Event::StartAuthenticate {
|
||||||
|
code_verifier: code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
},
|
||||||
|
) => Self::Authenticate {
|
||||||
|
code_verifier: code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
}, // Goto Authenticate
|
||||||
|
|
||||||
|
(State::AppStart { .. }, Event::TokenReceived { token }) => State::Connect { token },
|
||||||
|
(State::Authenticate { .. }, Event::TokenReceived { token }) => {
|
||||||
|
State::Connect { token }
|
||||||
|
}
|
||||||
|
|
||||||
|
(State::Connect { .. }, Event::Connected) => todo!(), // Goto WaitForSim
|
||||||
|
|
||||||
|
(State::WaitForSim, Event::SimConnected) => todo!(), // Goto Running
|
||||||
|
|
||||||
|
(State::Running, Event::Disconnected) => todo!(), // Goto Connect
|
||||||
|
(State::Running, Event::SimDisconnected) => todo!(), // Goto WaitForSim
|
||||||
|
|
||||||
|
(_, Event::Quit) => todo!(), // All events can go into quit, to shutdown the application
|
||||||
|
|
||||||
|
_ => panic!("Invalid state transition"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self, signal: Sender<Event>) -> Result<(), anyhow::Error> {
|
||||||
|
match self {
|
||||||
|
State::Init => Ok(()),
|
||||||
|
State::AppStart { config } => {
|
||||||
|
if let Some(token) = config.token() {
|
||||||
|
signal.send(Event::TokenReceived {
|
||||||
|
token: token.to_string(),
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
let code_verifier = CodeVerifier::new();
|
||||||
|
let code_challenge_method = CodeChallengeMethod::Sha256;
|
||||||
|
|
||||||
|
config.set_code_verifier(Some(code_verifier.clone()))?;
|
||||||
|
config.set_code_challenge_method(Some(code_challenge_method.clone()))?;
|
||||||
|
|
||||||
|
signal.send(Event::StartAuthenticate {
|
||||||
|
code_verifier,
|
||||||
|
code_challenge_method,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
State::Authenticate {
|
||||||
|
code_verifier,
|
||||||
|
code_challenge_method,
|
||||||
|
} => {
|
||||||
|
oauth::open_browser(code_verifier.clone(), code_challenge_method.clone())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
State::Connect { token } => {
|
||||||
|
println!("Holyshit we've got a token: {}", token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
State::WaitForSim => Ok(()),
|
||||||
|
State::Running => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
config: Config,
|
||||||
|
event_sender: Sender<Event>,
|
||||||
|
mut event_receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let mut state = State::Init;
|
||||||
|
|
||||||
|
state.run(event_sender.clone()).await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Ok(event) = event_receiver.recv().await {
|
||||||
|
if event == Event::Quit {
|
||||||
|
println!("Shutting down State Machine");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.next(event).await;
|
||||||
|
|
||||||
|
// before run
|
||||||
|
if let State::Connect { token } = &state {
|
||||||
|
// before run Connect, save the given token in config
|
||||||
|
config.set_token(Some(token.clone()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.run(event_sender.clone()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("State Machine Shutdown");
|
||||||
|
Ok(())
|
||||||
|
}
|
5
migrations/20241012185353_oauth2.down.sql
Normal file
5
migrations/20241012185353_oauth2.down.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE "refresh_tokens";
|
||||||
|
DROP TABLE "authorized_clients";
|
||||||
|
DROP TABLE "authorization_code";
|
||||||
|
DROP TABLE "clients";
|
40
migrations/20241012185353_oauth2.up.sql
Normal file
40
migrations/20241012185353_oauth2.up.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
|
||||||
|
CREATE TABLE "clients" (
|
||||||
|
"id" uuid NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"secret" text NOT NULL,
|
||||||
|
"redirect_uri" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "clients_name_key" UNIQUE ("name"),
|
||||||
|
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
CREATE TABLE "authorization_code" (
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"client_id" uuid NOT NULL,
|
||||||
|
"code_challenge" text NOT NULL,
|
||||||
|
"code_challenge_method" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "authorization_code_pkey" PRIMARY KEY ("code")
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
CREATE TABLE "authorized_clients" (
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"client_id" uuid NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, -- last accessed
|
||||||
|
CONSTRAINT "authorized_clients_pkey" PRIMARY KEY ("user_id", "client_id")
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
CREATE TABLE "refresh_tokens" (
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"client_id" uuid NOT NULL,
|
||||||
|
"expires_at" timestamp DEFAULT CURRENT_TIMESTAMP + interval '30' day,
|
||||||
|
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("token")
|
||||||
|
) WITH (oids = false);
|
@@ -1,4 +1,5 @@
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
#[allow(clippy::needless_return)]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
use avam::config::Config;
|
use avam::config::Config;
|
||||||
@@ -31,13 +32,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let app_state = AppState::new(api_service).await;
|
let app_state = AppState::new(api_service).await;
|
||||||
|
|
||||||
let http_server = HttpServer::new(app_state, postgres.pool()).await?;
|
HttpServer::new(app_state, postgres.pool())
|
||||||
http_server.run().await
|
.await?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
println!("Do run this main?!");
|
|
||||||
// no client-side main function
|
// no client-side main function
|
||||||
// unless we want this to work with e.g., Trunk for a purely client-side app
|
// unless we want this to work with e.g., Trunk for a purely client-side app
|
||||||
// see lib.rs for hydration function instead
|
// see lib.rs for hydration function instead
|
||||||
|
@@ -8,6 +8,7 @@ const SMTP_PORT: &str = "SMTP_PORT";
|
|||||||
const SMTP_USERNAME: &str = "SMTP_USERNAME";
|
const SMTP_USERNAME: &str = "SMTP_USERNAME";
|
||||||
const SMTP_PASSWORD: &str = "SMTP_PASSWORD";
|
const SMTP_PASSWORD: &str = "SMTP_PASSWORD";
|
||||||
const SMTP_SENDER: &str = "SMTP_SENDER";
|
const SMTP_SENDER: &str = "SMTP_SENDER";
|
||||||
|
const JWT_SECRET: &str = "JWT_SECRET";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -18,6 +19,8 @@ pub struct Config {
|
|||||||
pub smtp_username: String,
|
pub smtp_username: String,
|
||||||
pub smtp_password: String,
|
pub smtp_password: String,
|
||||||
pub smtp_sender: String,
|
pub smtp_sender: String,
|
||||||
|
|
||||||
|
pub jwt_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -28,6 +31,7 @@ impl Config {
|
|||||||
let smtp_username = load_env(SMTP_USERNAME)?;
|
let smtp_username = load_env(SMTP_USERNAME)?;
|
||||||
let smtp_password = load_env(SMTP_PASSWORD)?;
|
let smtp_password = load_env(SMTP_PASSWORD)?;
|
||||||
let smtp_sender = load_env(SMTP_SENDER)?;
|
let smtp_sender = load_env(SMTP_SENDER)?;
|
||||||
|
let jwt_secret = load_env(JWT_SECRET)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
database_url,
|
database_url,
|
||||||
@@ -36,6 +40,7 @@ impl Config {
|
|||||||
smtp_username,
|
smtp_username,
|
||||||
smtp_password,
|
smtp_password,
|
||||||
smtp_sender,
|
smtp_sender,
|
||||||
|
jwt_secret,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ pub mod prelude {
|
|||||||
// But so far, this is the only thing that actually works
|
// But so far, this is the only thing that actually works
|
||||||
pub type AppService = std::sync::Arc<Service<Postgres, DangerousLettre>>;
|
pub type AppService = std::sync::Arc<Service<Postgres, DangerousLettre>>;
|
||||||
|
|
||||||
|
pub use super::models::oauth::*;
|
||||||
pub use super::models::user::*;
|
pub use super::models::user::*;
|
||||||
pub use super::ports::*;
|
pub use super::ports::*;
|
||||||
pub use super::service::*;
|
pub use super::service::*;
|
||||||
@@ -32,6 +33,7 @@ pub mod prelude {
|
|||||||
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
pub use super::models::oauth::*;
|
||||||
pub use super::models::user::*;
|
pub use super::models::user::*;
|
||||||
pub use crate::domain::leptos::flashbag::Alert;
|
pub use crate::domain::leptos::flashbag::Alert;
|
||||||
pub use crate::domain::leptos::flashbag::Flash;
|
pub use crate::domain::leptos::flashbag::Flash;
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
|
pub mod oauth;
|
||||||
|
pub mod pilot;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
377
src/lib/domain/api/models/oauth.rs
Normal file
377
src/lib/domain/api/models/oauth.rs
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use derive_more::derive::{Display, From};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::user::User;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Client {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
name: ClientName,
|
||||||
|
secret: ClientSecret,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(
|
||||||
|
id: uuid::Uuid,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
name: ClientName,
|
||||||
|
secret: ClientSecret,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
secret,
|
||||||
|
redirect_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> uuid::Uuid {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &ClientName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri(&self) -> &RedirectUri {
|
||||||
|
&self.redirect_uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct ClientName(String);
|
||||||
|
|
||||||
|
impl ClientName {
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
Self(name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "String")]
|
||||||
|
pub struct ClientSecret(String);
|
||||||
|
|
||||||
|
impl From<String> for ClientSecret {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for ClientSecret {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl ClientSecret {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let token: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(32)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl Default for ClientSecret {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Display, From, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct RedirectUri(String);
|
||||||
|
|
||||||
|
impl RedirectUri {
|
||||||
|
pub fn new(uri: &str) -> Self {
|
||||||
|
Self(uri.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateAuthorizationCodeError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "String")]
|
||||||
|
pub struct AuthorizationCode(String);
|
||||||
|
|
||||||
|
impl From<String> for AuthorizationCode {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for AuthorizationCode {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl AuthorizationCode {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let token: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(64)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl Default for AuthorizationCode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizedClient {
|
||||||
|
client: Client,
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CodeChallengeMethodError {
|
||||||
|
#[error("Code challenge method is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum CodeChallengeMethod {
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "plain")]
|
||||||
|
Plain,
|
||||||
|
#[serde(rename = "S256")]
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CodeChallengeMethod {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
CodeChallengeMethod::Plain => "plain",
|
||||||
|
CodeChallengeMethod::Sha256 => "S256",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CodeChallengeMethod {
|
||||||
|
type Err = crate::domain::api::models::oauth::CodeChallengeMethodError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"plain" => Ok(Self::Plain),
|
||||||
|
"S256" => Ok(Self::Sha256),
|
||||||
|
_ => Err(crate::domain::api::models::oauth::CodeChallengeMethodError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for CodeChallengeMethod {
|
||||||
|
type Error = crate::domain::api::models::oauth::CodeChallengeMethodError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_str(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ResponseTypeError {
|
||||||
|
#[error("The response type is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum ResponseType {
|
||||||
|
#[serde(rename = "code")]
|
||||||
|
Code,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ResponseType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
ResponseType::Code => "code",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ResponseType {
|
||||||
|
type Err = crate::domain::api::models::oauth::ResponseTypeError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"code" => Ok(Self::Code),
|
||||||
|
_ => Err(crate::domain::api::models::oauth::ResponseTypeError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizeRequest {
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
response_type: ResponseType, // Make type (enum:code,)
|
||||||
|
state: Option<String>, // random string for CSRF protection
|
||||||
|
code_challenge: String, // pkce
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>, // Make type (enum:sha256,) hashing algo
|
||||||
|
redirect_uri: RedirectUri, // Make type
|
||||||
|
scope: Option<String>, // space seperated string with permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizeRequest {
|
||||||
|
pub fn new(
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
response_type: ResponseType,
|
||||||
|
state: Option<String>,
|
||||||
|
code_challenge: String,
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
scope: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id,
|
||||||
|
response_type,
|
||||||
|
state,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
redirect_uri,
|
||||||
|
scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response_type(&self) -> ResponseType {
|
||||||
|
self.response_type.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> Option<String> {
|
||||||
|
self.state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge(&self) -> String {
|
||||||
|
self.code_challenge.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
||||||
|
self.code_challenge_method.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri(&self) -> RedirectUri {
|
||||||
|
self.redirect_uri.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope(&self) -> Option<String> {
|
||||||
|
self.scope.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationResponse {
|
||||||
|
code: AuthorizationCode,
|
||||||
|
state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizationResponse {
|
||||||
|
pub fn new(code: AuthorizationCode, state: Option<String>) -> Self {
|
||||||
|
Self { code, state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code(&self) -> AuthorizationCode {
|
||||||
|
self.code.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> Option<String> {
|
||||||
|
self.state.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TokenError {
|
||||||
|
#[error("Invalid Token Request")]
|
||||||
|
InvalidRequest,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TokenSubject {
|
||||||
|
#[serde(rename = "a")]
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
#[serde(rename = "b")]
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
#[serde(skip)]
|
||||||
|
code_challenge: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenSubject {
|
||||||
|
pub fn new(
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_challenge: String,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id,
|
||||||
|
client_id,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> uuid::Uuid {
|
||||||
|
self.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge(&self) -> String {
|
||||||
|
self.code_challenge.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge_method(&self) -> CodeChallengeMethod {
|
||||||
|
self.code_challenge_method.clone()
|
||||||
|
}
|
||||||
|
}
|
0
src/lib/domain/api/models/pilot.rs
Normal file
0
src/lib/domain/api/models/pilot.rs
Normal file
@@ -40,8 +40,8 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> &uuid::Uuid {
|
pub fn id(&self) -> uuid::Uuid {
|
||||||
&self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn email(&self) -> &EmailAddress {
|
pub fn email(&self) -> &EmailAddress {
|
||||||
@@ -103,6 +103,13 @@ impl ActivationToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl Default for ActivationToken {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
|
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
|
||||||
#[serde(from = "String")]
|
#[serde(from = "String")]
|
||||||
@@ -135,6 +142,13 @@ impl PasswordResetToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl Default for PasswordResetToken {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ResetPasswordError {
|
pub enum ResetPasswordError {
|
||||||
@@ -410,6 +424,13 @@ impl UpdateUserRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl Default for UpdateUserRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum UpdateUserError {
|
pub enum UpdateUserError {
|
||||||
|
@@ -1,133 +1,9 @@
|
|||||||
/*
|
mod api_service;
|
||||||
Module `ports` specifies the API by which external modules interact with the user domain.
|
mod oauth_repository;
|
||||||
|
mod user_notifier;
|
||||||
|
mod user_repository;
|
||||||
|
|
||||||
All traits are bounded by `Send + Sync + 'static`, since their implementations must be shareable
|
pub use api_service::ApiService;
|
||||||
between request-handling threads.
|
pub use oauth_repository::OAuthRepository;
|
||||||
|
pub use user_notifier::UserNotifier;
|
||||||
Trait methods are explicitly asynchronous, including `Send` bounds on response types,
|
pub use user_repository::UserRepository;
|
||||||
since the application is expected to always run in a multithreaded environment.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
use super::models::user::*;
|
|
||||||
|
|
||||||
/// `ApiService` is the public API for the user domain.
|
|
||||||
///
|
|
||||||
/// External modules must conform to this contract – the domain is not concerned with the
|
|
||||||
/// implementation details or underlying technology of any external code.
|
|
||||||
pub trait ApiService: Clone + Send + Sync + 'static {
|
|
||||||
/// Asynchronously create a new [User].
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// - [CreateUserError::Duplicate] if an [User] with the same [EmailAddress] already exists.
|
|
||||||
fn create_user(
|
|
||||||
&self,
|
|
||||||
req: CreateUserRequest,
|
|
||||||
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
|
||||||
|
|
||||||
// fn activate_user
|
|
||||||
|
|
||||||
fn get_user_session(
|
|
||||||
&self,
|
|
||||||
session: &axum_session::SessionAnySession, // TODO: Get rid of this and make cleaner
|
|
||||||
) -> impl Future<Output = Option<User>> + Send;
|
|
||||||
|
|
||||||
fn activate_user_account(
|
|
||||||
&self,
|
|
||||||
token: ActivationToken,
|
|
||||||
) -> impl Future<Output = Result<User, ActivateUserError>> + Send;
|
|
||||||
|
|
||||||
fn user_login(
|
|
||||||
&self,
|
|
||||||
req: UserLoginRequest,
|
|
||||||
) -> impl Future<Output = Result<User, UserLoginError>> + Send;
|
|
||||||
|
|
||||||
fn forgot_password(&self, email: &EmailAddress) -> impl Future<Output = ()> + Send;
|
|
||||||
|
|
||||||
fn reset_password(
|
|
||||||
&self,
|
|
||||||
token: &PasswordResetToken,
|
|
||||||
password: &Password,
|
|
||||||
) -> impl Future<Output = Result<User, ResetPasswordError>> + Send;
|
|
||||||
|
|
||||||
fn find_user_by_password_reset_token(
|
|
||||||
&self,
|
|
||||||
token: &PasswordResetToken,
|
|
||||||
) -> impl Future<Output = Option<User>> + Send;
|
|
||||||
|
|
||||||
// These shouldnt be here, _why_ are they here, and implement that here instead
|
|
||||||
// fn find_user_by_email(self, email: EmailAddress) -> impl Future<Output = Option<User>> + Send;
|
|
||||||
// fn find_user_by_id(&self, user_id: uuid::Uuid) -> impl Future<Output = Option<User>> + Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait UserRepository: Clone + Send + Sync + 'static {
|
|
||||||
// Create
|
|
||||||
fn create_user(
|
|
||||||
&self,
|
|
||||||
req: CreateUserRequest,
|
|
||||||
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
|
||||||
|
|
||||||
fn create_activation_token(
|
|
||||||
&self,
|
|
||||||
ent: &User,
|
|
||||||
) -> impl Future<Output = Result<ActivationToken, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
fn create_password_reset_token(
|
|
||||||
&self,
|
|
||||||
ent: &User,
|
|
||||||
) -> impl Future<Output = Result<PasswordResetToken, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
// Read
|
|
||||||
fn all_users(&self) -> impl Future<Output = Vec<User>> + Send;
|
|
||||||
|
|
||||||
fn find_user_by_id(
|
|
||||||
&self,
|
|
||||||
id: uuid::Uuid,
|
|
||||||
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
fn find_user_by_email(
|
|
||||||
&self,
|
|
||||||
email: &EmailAddress,
|
|
||||||
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
fn find_user_by_activation_token(
|
|
||||||
&self,
|
|
||||||
token: &ActivationToken,
|
|
||||||
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
fn find_user_by_password_reset_token(
|
|
||||||
&self,
|
|
||||||
token: &PasswordResetToken,
|
|
||||||
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
// // Update
|
|
||||||
fn update_user(
|
|
||||||
&self,
|
|
||||||
ent: &User,
|
|
||||||
req: UpdateUserRequest,
|
|
||||||
) -> impl Future<Output = Result<(User, User), UpdateUserError>> + Send;
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
// fn delete_user(&self, ent: User) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
|
||||||
fn delete_activation_token_for_user(
|
|
||||||
&self,
|
|
||||||
ent: &User,
|
|
||||||
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
|
||||||
|
|
||||||
fn delete_password_reset_tokens_for_user(
|
|
||||||
&self,
|
|
||||||
ent: &User,
|
|
||||||
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait UserNotifier: Clone + Send + Sync + 'static {
|
|
||||||
fn user_created(&self, user: &User, token: &ActivationToken)
|
|
||||||
-> impl Future<Output = ()> + Send;
|
|
||||||
fn forgot_password(
|
|
||||||
&self,
|
|
||||||
user: &User,
|
|
||||||
token: &PasswordResetToken,
|
|
||||||
) -> impl Future<Output = ()> + Send;
|
|
||||||
}
|
|
||||||
|
60
src/lib/domain/api/ports/api_service.rs
Normal file
60
src/lib/domain/api/ports/api_service.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use crate::{
|
||||||
|
domain::api::models::oauth::*, inbound::http::handlers::oauth::AuthorizationCodeRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::models::user::*;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
pub trait ApiService: Clone + Send + Sync + 'static {
|
||||||
|
// ---
|
||||||
|
// USER
|
||||||
|
// ---
|
||||||
|
fn create_user(
|
||||||
|
&self,
|
||||||
|
req: CreateUserRequest,
|
||||||
|
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_session(
|
||||||
|
&self,
|
||||||
|
session: &axum_session::SessionAnySession, // TODO: Get rid of this and make cleaner
|
||||||
|
) -> impl Future<Output = Option<User>> + Send;
|
||||||
|
|
||||||
|
fn activate_user_account(
|
||||||
|
&self,
|
||||||
|
token: ActivationToken,
|
||||||
|
) -> impl Future<Output = Result<User, ActivateUserError>> + Send;
|
||||||
|
|
||||||
|
fn user_login(
|
||||||
|
&self,
|
||||||
|
req: UserLoginRequest,
|
||||||
|
) -> impl Future<Output = Result<User, UserLoginError>> + Send;
|
||||||
|
|
||||||
|
fn forgot_password(&self, email: &EmailAddress) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn reset_password(
|
||||||
|
&self,
|
||||||
|
token: &PasswordResetToken,
|
||||||
|
password: &Password,
|
||||||
|
) -> impl Future<Output = Result<User, ResetPasswordError>> + Send;
|
||||||
|
|
||||||
|
fn find_user_by_password_reset_token(
|
||||||
|
&self,
|
||||||
|
token: &PasswordResetToken,
|
||||||
|
) -> impl Future<Output = Option<User>> + Send;
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// OAUTH
|
||||||
|
// ---
|
||||||
|
fn find_client_by_id(&self, id: uuid::Uuid) -> impl Future<Output = Option<Client>> + Send;
|
||||||
|
|
||||||
|
fn generate_authorization_code(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
req: AuthorizeRequest,
|
||||||
|
) -> impl Future<Output = Result<AuthorizationResponse, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn create_token(
|
||||||
|
&self,
|
||||||
|
req: AuthorizationCodeRequest,
|
||||||
|
) -> impl Future<Output = Result<Option<TokenSubject>, TokenError>> + Send;
|
||||||
|
}
|
33
src/lib/domain/api/ports/oauth_repository.rs
Normal file
33
src/lib/domain/api/ports/oauth_repository.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use super::super::models::oauth::*;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
pub trait OAuthRepository: Clone + Send + Sync + 'static {
|
||||||
|
fn find_client_by_id(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
) -> impl Future<Output = Result<Option<Client>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn create_authorization_code(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_challenge: String,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
) -> impl Future<Output = Result<AuthorizationCode, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn is_authorized_client(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
) -> impl Future<Output = Result<bool, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn get_token_subject(
|
||||||
|
&self,
|
||||||
|
code: AuthorizationCode,
|
||||||
|
) -> impl Future<Output = Result<Option<TokenSubject>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn delete_token(
|
||||||
|
&self,
|
||||||
|
code: AuthorizationCode,
|
||||||
|
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
||||||
|
}
|
12
src/lib/domain/api/ports/user_notifier.rs
Normal file
12
src/lib/domain/api/ports/user_notifier.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use super::super::models::user::*;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
pub trait UserNotifier: Clone + Send + Sync + 'static {
|
||||||
|
fn user_created(&self, user: &User, token: &ActivationToken)
|
||||||
|
-> impl Future<Output = ()> + Send;
|
||||||
|
fn forgot_password(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
token: &PasswordResetToken,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
}
|
62
src/lib/domain/api/ports/user_repository.rs
Normal file
62
src/lib/domain/api/ports/user_repository.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use super::super::models::user::*;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
pub trait UserRepository: Clone + Send + Sync + 'static {
|
||||||
|
// Create
|
||||||
|
fn create_user(
|
||||||
|
&self,
|
||||||
|
req: CreateUserRequest,
|
||||||
|
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
||||||
|
|
||||||
|
fn create_activation_token(
|
||||||
|
&self,
|
||||||
|
ent: &User,
|
||||||
|
) -> impl Future<Output = Result<ActivationToken, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn create_password_reset_token(
|
||||||
|
&self,
|
||||||
|
ent: &User,
|
||||||
|
) -> impl Future<Output = Result<PasswordResetToken, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
// Read
|
||||||
|
fn all_users(&self) -> impl Future<Output = Vec<User>> + Send;
|
||||||
|
|
||||||
|
fn find_user_by_id(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn find_user_by_email(
|
||||||
|
&self,
|
||||||
|
email: &EmailAddress,
|
||||||
|
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn find_user_by_activation_token(
|
||||||
|
&self,
|
||||||
|
token: &ActivationToken,
|
||||||
|
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn find_user_by_password_reset_token(
|
||||||
|
&self,
|
||||||
|
token: &PasswordResetToken,
|
||||||
|
) -> impl Future<Output = Result<Option<User>, anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
// // Update
|
||||||
|
fn update_user(
|
||||||
|
&self,
|
||||||
|
ent: &User,
|
||||||
|
req: UpdateUserRequest,
|
||||||
|
) -> impl Future<Output = Result<(User, User), UpdateUserError>> + Send;
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// fn delete_user(&self, ent: User) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
||||||
|
fn delete_activation_token_for_user(
|
||||||
|
&self,
|
||||||
|
ent: &User,
|
||||||
|
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
||||||
|
|
||||||
|
fn delete_password_reset_tokens_for_user(
|
||||||
|
&self,
|
||||||
|
ent: &User,
|
||||||
|
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
||||||
|
}
|
@@ -4,17 +4,22 @@
|
|||||||
*/
|
*/
|
||||||
use axum_session::SessionAnySession;
|
use axum_session::SessionAnySession;
|
||||||
|
|
||||||
|
use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest;
|
||||||
|
use crate::inbound::http::handlers::oauth::GrantType;
|
||||||
|
|
||||||
|
use super::models::oauth::Client;
|
||||||
|
use super::models::oauth::*;
|
||||||
use super::models::user::*;
|
use super::models::user::*;
|
||||||
|
|
||||||
use super::ports::{ApiService, UserNotifier, UserRepository};
|
use super::ports::{ApiService, OAuthRepository, UserNotifier, UserRepository};
|
||||||
|
|
||||||
pub trait Repository = UserRepository;
|
// pub trait Repository = UserRepository + OAuthRepository;
|
||||||
pub trait Email = UserNotifier;
|
pub trait Email = UserNotifier;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Service<R, N>
|
pub struct Service<R, N>
|
||||||
where
|
where
|
||||||
R: Repository,
|
R: UserRepository + OAuthRepository,
|
||||||
N: Email,
|
N: Email,
|
||||||
{
|
{
|
||||||
repo: R,
|
repo: R,
|
||||||
@@ -23,7 +28,7 @@ where
|
|||||||
|
|
||||||
impl<R, N> Service<R, N>
|
impl<R, N> Service<R, N>
|
||||||
where
|
where
|
||||||
R: Repository,
|
R: UserRepository + OAuthRepository,
|
||||||
N: Email,
|
N: Email,
|
||||||
{
|
{
|
||||||
pub fn new(repo: R, notifier: N) -> Self {
|
pub fn new(repo: R, notifier: N) -> Self {
|
||||||
@@ -33,12 +38,13 @@ where
|
|||||||
|
|
||||||
impl<R, N> ApiService for Service<R, N>
|
impl<R, N> ApiService for Service<R, N>
|
||||||
where
|
where
|
||||||
R: UserRepository,
|
R: UserRepository + OAuthRepository,
|
||||||
N: Email,
|
N: Email,
|
||||||
{
|
{
|
||||||
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
||||||
let result = self.repo.create_user(req).await;
|
let result = self.repo.create_user(req).await;
|
||||||
|
|
||||||
|
#[allow(clippy::question_mark)]
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
// something went wrong, log the error
|
// something went wrong, log the error
|
||||||
// but keep passing on the result to the requester (http server)
|
// but keep passing on the result to the requester (http server)
|
||||||
@@ -56,9 +62,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_session(&self, session: &SessionAnySession) -> Option<User> {
|
async fn get_user_session(&self, session: &SessionAnySession) -> Option<User> {
|
||||||
let Some(user_id) = session.get("user") else {
|
let user_id = session.get("user")?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.repo.find_user_by_id(user_id).await.unwrap_or(None)
|
self.repo.find_user_by_id(user_id).await.unwrap_or(None)
|
||||||
}
|
}
|
||||||
@@ -69,7 +73,7 @@ where
|
|||||||
) -> Result<User, ActivateUserError> {
|
) -> Result<User, ActivateUserError> {
|
||||||
let user = match self.repo.find_user_by_activation_token(&token).await {
|
let user = match self.repo.find_user_by_activation_token(&token).await {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => return Err(ActivateUserError::Unknown(e.into())),
|
Err(e) => return Err(ActivateUserError::Unknown(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(user) = user else {
|
let Some(user) = user else {
|
||||||
@@ -98,7 +102,7 @@ where
|
|||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("{:#?}", e);
|
tracing::error!("{:#?}", e);
|
||||||
return Err(UserLoginError::Unknown(e.into()));
|
return Err(UserLoginError::Unknown(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,7 +166,7 @@ where
|
|||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("{:#?}", e);
|
tracing::error!("{:#?}", e);
|
||||||
return Err(ResetPasswordError::Unknown(e.into()));
|
return Err(ResetPasswordError::Unknown(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,4 +194,77 @@ where
|
|||||||
.await
|
.await
|
||||||
.unwrap_or(None)
|
.unwrap_or(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_client_by_id(&self, id: uuid::Uuid) -> Option<Client> {
|
||||||
|
self.repo.find_client_by_id(id).await.ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_authorization_code(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
req: AuthorizeRequest,
|
||||||
|
) -> Result<AuthorizationResponse, anyhow::Error> {
|
||||||
|
let Some(client) = self.repo.find_client_by_id(req.client_id()).await? else {
|
||||||
|
return Err(anyhow::anyhow!("Client not found"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if client.redirect_uri() != &req.redirect_uri() {
|
||||||
|
return Err(anyhow::anyhow!("Invalid redirect uri"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = self
|
||||||
|
.repo
|
||||||
|
.create_authorization_code(
|
||||||
|
user.id(),
|
||||||
|
client.id(),
|
||||||
|
req.code_challenge(),
|
||||||
|
req.code_challenge_method().unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(AuthorizationResponse::new(code, req.state()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_token(
|
||||||
|
&self,
|
||||||
|
req: AuthorizationCodeRequest,
|
||||||
|
) -> Result<Option<TokenSubject>, TokenError> {
|
||||||
|
if req.grant_type() != GrantType::AuthorizationCode {
|
||||||
|
return Err(TokenError::InvalidRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = req.code();
|
||||||
|
|
||||||
|
let Some(token) = self.repo.get_token_subject(code).await? else {
|
||||||
|
return Err(TokenError::InvalidRequest);
|
||||||
|
};
|
||||||
|
|
||||||
|
let code_verifier = req.code_verifier();
|
||||||
|
let code_challenge = match token.code_challenge_method() {
|
||||||
|
CodeChallengeMethod::Plain => {
|
||||||
|
use base64::prelude::*;
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string())
|
||||||
|
}
|
||||||
|
CodeChallengeMethod::Sha256 => {
|
||||||
|
use base64::prelude::*;
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if token.code_challenge() != code_challenge {
|
||||||
|
return Err(TokenError::InvalidRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(client) = self.repo.find_client_by_id(token.client_id()).await? else {
|
||||||
|
return Err(TokenError::InvalidRequest); // no such client
|
||||||
|
};
|
||||||
|
|
||||||
|
if &req.redirect_uri() != client.redirect_uri() {
|
||||||
|
return Err(TokenError::InvalidRequest); // invalid redirect uri
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.repo.delete_token(req.code()).await;
|
||||||
|
|
||||||
|
Ok(Some(token))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,16 +11,23 @@ use pages::{
|
|||||||
},
|
},
|
||||||
dashboard::DashboardPage,
|
dashboard::DashboardPage,
|
||||||
error::{AppError, ErrorTemplate},
|
error::{AppError, ErrorTemplate},
|
||||||
|
oauth2::authorize::AuthorizePage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::domain::api::prelude::User;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
|
|
||||||
let trigger_user = create_rw_signal(true);
|
let trigger_update = create_rw_signal(false);
|
||||||
|
let trigger_direct = create_rw_signal(None::<String>);
|
||||||
|
|
||||||
let user = create_resource(trigger_user, move |_| async move {
|
let user_signal = create_rw_signal(None::<User>);
|
||||||
super::check_user().await.unwrap()
|
|
||||||
|
let user_resource = create_local_resource(trigger_update, move |_| async move {
|
||||||
|
let user = super::check_user().await.unwrap();
|
||||||
|
user_signal.set(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -47,17 +54,17 @@ pub fn App() -> impl IntoView {
|
|||||||
}>
|
}>
|
||||||
<main class="h-screen overflow-auto dark:base-100 dark:text-white">
|
<main class="h-screen overflow-auto dark:base-100 dark:text-white">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/auth" view=move || {
|
<Route path="auth" view=move || {
|
||||||
view! {
|
view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || user().is_some_and(|u| u.is_some())>
|
<Show when=move || user_resource().is_some_and(|_|user_signal().is_some())>
|
||||||
<Redirect path="/" />
|
<Redirect path={ trigger_direct().unwrap_or(String::from("/")) } />
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
<Route path="login" view=move || view! { <LoginPage user_signal=trigger_user /> } />
|
<Route path="login" view=move || view! { <LoginPage trigger_signal=trigger_update direct_signal=trigger_direct /> } />
|
||||||
<Route path="register" view=RegisterPage />
|
<Route path="register" view=RegisterPage />
|
||||||
<Route path="forgot" view=ForgotPage />
|
<Route path="forgot" view=ForgotPage />
|
||||||
<Route path="reset/:token" view=ResetPage />
|
<Route path="reset/:token" view=ResetPage />
|
||||||
@@ -66,8 +73,11 @@ pub fn App() -> impl IntoView {
|
|||||||
<Route path="" view=move || {
|
<Route path="" view=move || {
|
||||||
view! {
|
view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || user().is_some_and(|u| u.is_none())>
|
<Show when=move || user_resource().is_some_and(|_|user_signal().is_none())>
|
||||||
<Redirect path="/auth/login" />
|
<Redirect path={
|
||||||
|
use base64::prelude::*;
|
||||||
|
format!("/auth/login?c={}", BASE64_URL_SAFE_NO_PAD.encode(format!("{}{}", (leptos_router::use_location().pathname)(), (leptos_router::use_location().query)().to_query_string())))
|
||||||
|
} />
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -75,13 +85,22 @@ pub fn App() -> impl IntoView {
|
|||||||
}>
|
}>
|
||||||
<Route path="" view=move || view! {
|
<Route path="" view=move || view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || user().is_some_and(|u| u.is_some())>
|
<Show when=move || user_signal().is_some()>
|
||||||
<DashboardPage user={ user().unwrap().unwrap() } />
|
<DashboardPage user={ user_signal().unwrap() } />
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
} />
|
} />
|
||||||
</Route> // dashboard
|
|
||||||
<Route path="/auth/logout" view=move || view! { <LogoutPage user_signal=trigger_user /> } />
|
<Route path="oauth2/authorize" view=move || view! {
|
||||||
|
<Suspense>
|
||||||
|
<Show when=move || user_signal().is_some()>
|
||||||
|
<AuthorizePage user={ user_signal } />
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
|
} />
|
||||||
|
|
||||||
|
</Route> // Logged in
|
||||||
|
<Route path="auth/logout" view=move || view! { <LogoutPage trigger_signal=trigger_update user_signal /> } />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
use crate::domain::leptos::app::components::alert::Alert;
|
use crate::domain::api::prelude::*;
|
||||||
|
|
||||||
use super::auth_base::AuthBase;
|
use super::auth_base::AuthBase;
|
||||||
|
|
||||||
|
use crate::domain::leptos::app::components::alert::Alert as AlertView;
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn login_action(email: String, password: String) -> Result<(), ServerFnError<String>> {
|
async fn login_action(email: String, password: String) -> Result<(), ServerFnError<String>> {
|
||||||
use crate::domain::api::prelude::*;
|
use crate::domain::api::prelude::*;
|
||||||
@@ -26,13 +28,27 @@ async fn login_action(email: String, password: String) -> Result<(), ServerFnErr
|
|||||||
|
|
||||||
/// Renders the home page of your application.
|
/// Renders the home page of your application.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LoginPage(user_signal: RwSignal<bool>) -> impl IntoView {
|
pub fn LoginPage(trigger_signal: RwSignal<bool>, direct_signal: RwSignal<Option<String>>) -> impl IntoView {
|
||||||
let submit = Action::<LoginAction, _>::server();
|
let submit = Action::<LoginAction, _>::server();
|
||||||
let response = submit.value().read_only();
|
let response = submit.value().read_only();
|
||||||
|
|
||||||
|
|
||||||
|
let callback = move || {
|
||||||
|
let query = use_query_map();
|
||||||
|
let Some(c) = query.with(|q| q.get("c").cloned()) else {
|
||||||
|
return Some("/".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::prelude::*;
|
||||||
|
let dec = BASE64_URL_SAFE_NO_PAD.decode(c).unwrap_or_default();
|
||||||
|
let c = String::from_utf8_lossy(&dec);
|
||||||
|
Some(c.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if response.get().is_some() {
|
if response.get().is_some() {
|
||||||
user_signal.set(!user_signal.get_untracked());
|
trigger_signal.set(!trigger_signal.get_untracked());
|
||||||
|
direct_signal.set(callback());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,25 +66,22 @@ pub fn LoginPage(user_signal: RwSignal<bool>) -> impl IntoView {
|
|||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || response.get().is_some_and(|e| e.is_err())>
|
<Show when=move || response.get().is_some_and(|e| e.is_err())>
|
||||||
<div role="alert" class="alert alert-error my-2">
|
<AlertView alert={Alert::Error}>
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
|
||||||
<span>
|
|
||||||
{ move || if let Some(Err(e)) = response.get() {
|
{ move || if let Some(Err(e)) = response.get() {
|
||||||
{format!("{}", e)}.into_view()
|
{format!("{}", e)}.into_view()
|
||||||
} else {
|
} else {
|
||||||
().into_view()
|
().into_view()
|
||||||
}}
|
}}
|
||||||
</span>
|
</AlertView>
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || flash().is_some_and(|u| u.is_some())>
|
<Show when=move || flash().is_some_and(|u| u.is_some())>
|
||||||
|
|
||||||
<Alert alert={ flash().unwrap().unwrap().alert() }>
|
<AlertView alert={ flash().unwrap().unwrap().alert() }>
|
||||||
{ flash().unwrap().unwrap().message() }
|
{ flash().unwrap().unwrap().message() }
|
||||||
</Alert>
|
</AlertView>
|
||||||
|
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::Redirect;
|
use leptos_router::{use_query_map, Redirect};
|
||||||
|
|
||||||
|
use crate::domain::api::prelude::User;
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn logout_action() -> Result<(), ServerFnError<String>> {
|
async fn logout_action() -> Result<(), ServerFnError<String>> {
|
||||||
@@ -12,22 +14,31 @@ async fn logout_action() -> Result<(), ServerFnError<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LogoutPage(user_signal: RwSignal<bool>) -> impl IntoView {
|
pub fn LogoutPage(
|
||||||
let submit = Action::<LogoutAction, _>::server();
|
trigger_signal: RwSignal<bool>,
|
||||||
let response = submit.value().read_only();
|
user_signal: RwSignal<Option<User>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let direct_signal = create_rw_signal(None::<String>);
|
||||||
|
|
||||||
create_effect(move |_| {
|
create_local_resource(
|
||||||
if response.get().is_some() {
|
|| (),
|
||||||
user_signal.set(!user_signal.get_untracked());
|
move |_| async move {
|
||||||
|
logout_action().await.unwrap();
|
||||||
|
trigger_signal.set(!trigger_signal.get_untracked());
|
||||||
|
|
||||||
|
let query = use_query_map();
|
||||||
|
if let Some(c) = query.with_untracked(|q| q.get("c").cloned()) {
|
||||||
|
direct_signal.set(Some(c));
|
||||||
|
} else {
|
||||||
|
direct_signal.set(Some("Lw".to_string()));
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
submit.dispatch(LogoutAction {});
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when=move || response().is_some()>
|
<Show when=move || (direct_signal().is_some() && user_signal().is_none())>
|
||||||
<Redirect path="/" />
|
<Redirect path={ format!("/auth/login?c={}", direct_signal().unwrap()) } />
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
|
@@ -19,9 +19,7 @@ async fn reset_action(token: String, password: String, confirm_password: String)
|
|||||||
app.reset_password(&token.into(), &password).await.map_err(|e| format!("{}", e))?;
|
app.reset_password(&token.into(), &password).await.map_err(|e| format!("{}", e))?;
|
||||||
|
|
||||||
let flash = FlashMessage::new("login",
|
let flash = FlashMessage::new("login",
|
||||||
format!(
|
"Your password has been reset.".to_string()).with_alert(Alert::Success);
|
||||||
"Your password has been reset."
|
|
||||||
)).with_alert(Alert::Success);
|
|
||||||
|
|
||||||
flashbag.set(flash);
|
flashbag.set(flash);
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod oauth2;
|
||||||
|
279
src/lib/domain/leptos/app/pages/oauth2/authorize.rs
Normal file
279
src/lib/domain/leptos/app/pages/oauth2/authorize.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::domain::api::prelude::*;
|
||||||
|
use crate::domain::leptos::app::components::alert::Alert as AlertView;
|
||||||
|
|
||||||
|
use crate::domain::leptos::app::pages::auth::auth_base::AuthBase;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct AuthorizationResponse {
|
||||||
|
code: String,
|
||||||
|
state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn authorize_action(form: AuthorizeQuery) -> Result<(), ServerFnError<String>> {
|
||||||
|
use crate::domain::api::prelude::*;
|
||||||
|
use crate::domain::leptos::check_user;
|
||||||
|
|
||||||
|
let app = use_context::<AppService>().unwrap();
|
||||||
|
|
||||||
|
let Some(client) = app.find_client_by_id(form.client_id()).await else {
|
||||||
|
return Err(ServerFnError::WrappedServerError(
|
||||||
|
"Invalid Client ID".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(user) = check_user().await? else {
|
||||||
|
return Err(ServerFnError::WrappedServerError("No user".to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: AuthorizationResponse = app
|
||||||
|
.generate_authorization_code(&user, form.into())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{}", e))?;
|
||||||
|
|
||||||
|
let qs = serde_qs::to_string(&response).map_err(|e| format!("{}", e))?;
|
||||||
|
|
||||||
|
leptos_axum::redirect(&format!("{}?{}", client.redirect_uri(), qs));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoAttribute for CodeChallengeMethod {
|
||||||
|
fn into_attribute(self) -> Attribute {
|
||||||
|
Attribute::String(self.to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||||
|
Attribute::String(self.to_string().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the types and validators for specific shit like response type and code challenge method
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct AuthorizeQuery {
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
response_type: ResponseType, // Make type (enum:code,)
|
||||||
|
state: Option<String>, // random string for CSRF protection
|
||||||
|
code_challenge: String, // pkce
|
||||||
|
code_challenge_method: Option<CodeChallengeMethod>, // Make type (enum:sha256,) hashing algo
|
||||||
|
redirect_uri: RedirectUri, // Make type
|
||||||
|
scope: Option<String>, // space seperated string with permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthorizeQuery> for AuthorizeRequest {
|
||||||
|
fn from(value: AuthorizeQuery) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value.client_id(),
|
||||||
|
value.response_type(),
|
||||||
|
value.state(),
|
||||||
|
value.code_challenge(),
|
||||||
|
value.code_challenge_method(),
|
||||||
|
value.redirect_uri(),
|
||||||
|
value.scope(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizeQuery {
|
||||||
|
pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response_type(&self) -> ResponseType {
|
||||||
|
self.response_type.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> Option<String> {
|
||||||
|
self.state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge(&self) -> String {
|
||||||
|
self.code_challenge.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
||||||
|
self.code_challenge_method.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri(&self) -> RedirectUri {
|
||||||
|
self.redirect_uri.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope(&self) -> Option<String> {
|
||||||
|
self.scope.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Params for AuthorizeQuery {
|
||||||
|
fn from_map(map: &ParamsMap) -> Result<Self, ParamsError> {
|
||||||
|
let client_id: uuid::Uuid = match map.get("client_id") {
|
||||||
|
Some(c) => uuid::Uuid::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e)))?,
|
||||||
|
None => return Err(ParamsError::MissingParam("client_id".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_type: ResponseType = match map.get("response_type") {
|
||||||
|
Some(c) => ResponseType::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e)))?,
|
||||||
|
None => return Err(ParamsError::MissingParam("response_type".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = map.get("state").cloned();
|
||||||
|
|
||||||
|
let Some(code_challenge) = map.get("code_challenge").cloned() else {
|
||||||
|
return Err(ParamsError::MissingParam("code_challenge".to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let code_challenge_method = map
|
||||||
|
.get("code_challenge_method")
|
||||||
|
.map(|c| CodeChallengeMethod::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e))))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let redirect_uri: RedirectUri = match map.get("redirect_uri") {
|
||||||
|
Some(c) => RedirectUri::new(c),
|
||||||
|
None => return Err(ParamsError::MissingParam("redirect_uri".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let scope = map.get("scope").cloned();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client_id,
|
||||||
|
response_type,
|
||||||
|
state,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
redirect_uri,
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn get_client(client_id: uuid::Uuid) -> Result<Client, ServerFnError<String>> {
|
||||||
|
use crate::domain::api::prelude::*;
|
||||||
|
|
||||||
|
let app = use_context::<AppService>().unwrap();
|
||||||
|
|
||||||
|
let Some(client) = app.find_client_by_id(client_id).await else {
|
||||||
|
return Err(ServerFnError::WrappedServerError(
|
||||||
|
"Invalid Client ID".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn AuthorizePage(user: RwSignal<Option<User>>) -> impl IntoView {
|
||||||
|
let submit = Action::<AuthorizeAction, _>::server();
|
||||||
|
let response = submit.value().read_only();
|
||||||
|
|
||||||
|
let query = use_query::<AuthorizeQuery>();
|
||||||
|
|
||||||
|
let client_signal = create_rw_signal(None::<Client>);
|
||||||
|
|
||||||
|
let client_resource = create_local_resource(
|
||||||
|
|| (),
|
||||||
|
move |_| async move {
|
||||||
|
let query = query.get_untracked().map_err(|e| format!("{}", e))?;
|
||||||
|
let client = get_client(query.client_id())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{}", e))?;
|
||||||
|
|
||||||
|
if client.redirect_uri() != &query.redirect_uri() {
|
||||||
|
return Err("Invalid redirect uri".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
client_signal.set(Some(client));
|
||||||
|
|
||||||
|
Ok::<(), String>(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<AuthBase>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<Show when=move || response.get().is_some_and(|e| e.is_err())>
|
||||||
|
<AlertView alert={Alert::Error}>
|
||||||
|
{ move || if let Some(Err(e)) = response.get() {
|
||||||
|
{format!("{}", e)}.into_view()
|
||||||
|
} else {
|
||||||
|
().into_view()
|
||||||
|
}}
|
||||||
|
</AlertView>
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<Show when=move || query.get().is_err()>
|
||||||
|
<AlertView alert={Alert::Error}>
|
||||||
|
{ move || if let Err(e) = query.get().map_err(|e| { leptos::logging::warn!("{:#?}", e); "Invalid parameters".to_string() }) {
|
||||||
|
{e.to_string()}.into_view()
|
||||||
|
} else {
|
||||||
|
().into_view()
|
||||||
|
}}
|
||||||
|
</AlertView>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<Show when=move || client_resource.get().is_some_and(|e| e.is_err())>
|
||||||
|
<AlertView alert={Alert::Error}>
|
||||||
|
{ move || if let Some(Err(e)) = client_resource.get() {
|
||||||
|
{e.to_string()}.into_view()
|
||||||
|
} else {
|
||||||
|
().into_view()
|
||||||
|
}}
|
||||||
|
</AlertView>
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<Show when=move || query.get().is_ok() && client_resource.get().is_some_and(|e| e.is_ok())>
|
||||||
|
<ActionForm action=submit class="w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="text-center text-sm">
|
||||||
|
"Signed in as "{move || user.get().unwrap().email().to_string()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when=move || response().is_some_and(|e| e.is_ok())>
|
||||||
|
<AlertView alert={Alert::Success}>
|
||||||
|
"You can now close this window."
|
||||||
|
</AlertView>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when=move || response().is_none()>
|
||||||
|
<div class="text-center text-sm">
|
||||||
|
<strong>{move || client_signal().unwrap().name().to_string() }</strong>" is requesting access to your account"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="hidden" name="form[client_id]" value=move || query().unwrap().client_id().to_string() />
|
||||||
|
<input type="hidden" name="form[response_type]" value=move || query().unwrap().response_type().to_string() />
|
||||||
|
<input type="hidden" name="form[state]" value=move || query().unwrap().state() />
|
||||||
|
<input type="hidden" name="form[code_challenge]" value=move || query().unwrap().code_challenge().to_string() />
|
||||||
|
<input type="hidden" name="form[code_challenge_method]" value=move || query().unwrap().code_challenge_method() />
|
||||||
|
<input type="hidden" name="form[redirect_uri]" value=move || query().unwrap().redirect_uri().to_string() />
|
||||||
|
<input type="hidden" name="form[scope]" value=move || query().unwrap().scope() />
|
||||||
|
<input type="submit" value="Authorize" class="btn btn-primary btn-block" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-sm">
|
||||||
|
"Not "{move || user.get().unwrap().email().to_string()}"? "<a href={move || {
|
||||||
|
use base64::prelude::*;
|
||||||
|
format!("/auth/logout?c={}", BASE64_URL_SAFE_NO_PAD.encode(format!("{}{}", (leptos_router::use_location().pathname)(), (leptos_router::use_location().query)().to_query_string())))
|
||||||
|
}} class="link">"Logout"</a>"!"
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</ActionForm>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
</AuthBase>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
1
src/lib/domain/leptos/app/pages/oauth2/mod.rs
Normal file
1
src/lib/domain/leptos/app/pages/oauth2/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod authorize;
|
@@ -46,7 +46,7 @@ where
|
|||||||
pub fn new(name: &str, message: S) -> Self {
|
pub fn new(name: &str, message: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
message: message.into(),
|
message,
|
||||||
alert: Alert::None,
|
alert: Alert::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,14 +82,12 @@ impl FlashBag {
|
|||||||
pub fn get(&self, flash_name: &str) -> Option<Flash> {
|
pub fn get(&self, flash_name: &str) -> Option<Flash> {
|
||||||
let name = format!("__flash:{}__", flash_name);
|
let name = format!("__flash:{}__", flash_name);
|
||||||
|
|
||||||
let Some(message) = self.session.get(&name) else {
|
let message = self.session.get(&name)?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let alert_name = format!("__flash_alert:{}__", flash_name);
|
let alert_name = format!("__flash_alert:{}__", flash_name);
|
||||||
let alert = self.session.get(&alert_name).unwrap_or(Alert::None);
|
let alert = self.session.get(&alert_name).unwrap_or(Alert::None);
|
||||||
|
|
||||||
self.clear(&flash_name);
|
self.clear(flash_name);
|
||||||
|
|
||||||
Some(Flash {
|
Some(Flash {
|
||||||
name,
|
name,
|
||||||
|
@@ -12,9 +12,7 @@ use axum::{
|
|||||||
use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore};
|
use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore};
|
||||||
use axum_session_sqlx::SessionPgPool;
|
use axum_session_sqlx::SessionPgPool;
|
||||||
use handlers::{
|
use handlers::{
|
||||||
fileserv::file_and_error_handler,
|
fileserv::file_and_error_handler, leptos::{leptos_routes_handler, server_fn_handler}, oauth, user::activate_account
|
||||||
leptos::{leptos_routes_handler, server_fn_handler},
|
|
||||||
user::activate_account,
|
|
||||||
};
|
};
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
@@ -59,6 +57,7 @@ impl HttpServer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let router = axum::Router::new()
|
let router = axum::Router::new()
|
||||||
|
.nest("/oauth2", oauth::routes())
|
||||||
.route("/auth/activate/:token", get(activate_account))
|
.route("/auth/activate/:token", get(activate_account))
|
||||||
.route("/api/*fn_name", post(server_fn_handler))
|
.route("/api/*fn_name", post(server_fn_handler))
|
||||||
.leptos_routes_with_handler(generate_route_list(App), get(leptos_routes_handler))
|
.leptos_routes_with_handler(generate_route_list(App), get(leptos_routes_handler))
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
pub mod fileserv;
|
pub mod fileserv;
|
||||||
pub mod leptos;
|
pub mod leptos;
|
||||||
|
pub mod oauth;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
192
src/lib/inbound/http/handlers/oauth.rs
Normal file
192
src/lib/inbound/http/handlers/oauth.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use axum::{extract::State, routing::post, Json, Router};
|
||||||
|
use derive_more::derive::Display;
|
||||||
|
use http::StatusCode;
|
||||||
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{config::Config, domain::api::prelude::*, inbound::http::state::AppState};
|
||||||
|
|
||||||
|
pub fn routes<S>() -> axum::Router<AppState<S>>
|
||||||
|
where
|
||||||
|
S: ApiService,
|
||||||
|
{
|
||||||
|
Router::new().route("/token", post(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GrantTypeError {
|
||||||
|
#[error("The grant type is not valid.")]
|
||||||
|
Invalid,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
// to be extended as new error scenarios are introduced
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum GrantType {
|
||||||
|
#[serde(rename = "authorization_code")]
|
||||||
|
AuthorizationCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GrantType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
GrantType::AuthorizationCode => "authorization_code",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for GrantType {
|
||||||
|
type Err = GrantTypeError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"authorization_code" => Ok(Self::AuthorizationCode),
|
||||||
|
_ => Err(GrantTypeError::Invalid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "String")]
|
||||||
|
pub struct CodeVerifier(String);
|
||||||
|
|
||||||
|
impl From<String> for CodeVerifier {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for CodeVerifier {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeVerifier {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let token: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(120)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeVerifier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationCodeRequest {
|
||||||
|
grant_type: GrantType,
|
||||||
|
code: AuthorizationCode,
|
||||||
|
redirect_uri: RedirectUri,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_verifier: CodeVerifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthorizationCodeRequest {
|
||||||
|
pub fn grant_type(&self) -> GrantType {
|
||||||
|
self.grant_type.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code(&self) -> AuthorizationCode {
|
||||||
|
self.code.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri(&self) -> RedirectUri {
|
||||||
|
self.redirect_uri.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_verifier(&self) -> CodeVerifier {
|
||||||
|
self.code_verifier.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TokenClaims<T>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
pub sub: T,
|
||||||
|
pub iat: usize,
|
||||||
|
pub exp: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationCodeResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn token<S: ApiService>(
|
||||||
|
State(app_state): State<AppState<S>>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Json<AuthorizationCodeResponse>, (StatusCode, String)> {
|
||||||
|
let request: AuthorizationCodeRequest = serde_qs::from_str(&body).map_err(|e| {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
String::from("Internal Server Error"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let app = app_state.api_service();
|
||||||
|
let token = app.create_token(request).await.map_err(|e| {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
String::from("Internal Server Error"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token = create_token(token).map_err(|e| {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
String::from("Internal Server Error"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(AuthorizationCodeResponse { token }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_token<T>(sub: T) -> Result<String, anyhow::Error>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
let config = Config::from_env()?;
|
||||||
|
|
||||||
|
let now = time::OffsetDateTime::now_utc();
|
||||||
|
let iat = now.unix_timestamp() as usize;
|
||||||
|
let exp = (now + time::Duration::days(30)).unix_timestamp() as usize;
|
||||||
|
|
||||||
|
let claims = TokenClaims { sub, iat, exp };
|
||||||
|
|
||||||
|
let token = encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claims,
|
||||||
|
&EncodingKey::from_secret(config.jwt_secret.as_bytes()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
@@ -1,7 +1,9 @@
|
|||||||
#![feature(trait_alias)]
|
#![feature(trait_alias)]
|
||||||
|
|
||||||
|
pub static BASE_URL: &str = "https://avam.avii.nl";
|
||||||
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
||||||
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
||||||
|
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
@@ -3,6 +3,7 @@ use lettre::AsyncTransport;
|
|||||||
use crate::domain::api::ports::UserNotifier;
|
use crate::domain::api::ports::UserNotifier;
|
||||||
|
|
||||||
use crate::domain::api::models::user::*;
|
use crate::domain::api::models::user::*;
|
||||||
|
use crate::BASE_URL;
|
||||||
|
|
||||||
use super::DangerousLettre;
|
use super::DangerousLettre;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ impl UserNotifier for DangerousLettre {
|
|||||||
async fn user_created(&self, user: &User, token: &ActivationToken) {
|
async fn user_created(&self, user: &User, token: &ActivationToken) {
|
||||||
let mut context = tera::Context::new();
|
let mut context = tera::Context::new();
|
||||||
|
|
||||||
let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env
|
let url = format!("{}/auth/activate/{}", BASE_URL, token); // Move base url to env
|
||||||
|
|
||||||
context.insert("activate_url", &url);
|
context.insert("activate_url", &url);
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ impl UserNotifier for DangerousLettre {
|
|||||||
async fn forgot_password(&self, user: &User, token: &PasswordResetToken) {
|
async fn forgot_password(&self, user: &User, token: &PasswordResetToken) {
|
||||||
let mut context = tera::Context::new();
|
let mut context = tera::Context::new();
|
||||||
|
|
||||||
let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env
|
let url = format!("{}/auth/reset/{}", BASE_URL, token); // Move base url to env
|
||||||
|
|
||||||
context.insert("reset_url", &url);
|
context.insert("reset_url", &url);
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod oauth_repository;
|
||||||
pub mod user_repository;
|
pub mod user_repository;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
179
src/lib/outbound/postgres/oauth_repository.rs
Normal file
179
src/lib/outbound/postgres/oauth_repository.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use sqlx::Executor;
|
||||||
|
// use sqlx::QueryBuilder;
|
||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
use crate::domain::api::models::oauth::*;
|
||||||
|
use crate::domain::api::ports::OAuthRepository;
|
||||||
|
|
||||||
|
use super::Postgres;
|
||||||
|
|
||||||
|
impl OAuthRepository for Postgres {
|
||||||
|
async fn find_client_by_id(&self, id: uuid::Uuid) -> Result<Option<Client>, anyhow::Error> {
|
||||||
|
let query = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
secret,
|
||||||
|
redirect_uri
|
||||||
|
FROM clients WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id);
|
||||||
|
|
||||||
|
let row = self
|
||||||
|
.pool
|
||||||
|
.fetch_optional(query)
|
||||||
|
.await
|
||||||
|
.context("failed to execute SQL transaction")?;
|
||||||
|
|
||||||
|
let Some(row) = row else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = row.get("id");
|
||||||
|
let user_id = row.get("user_id");
|
||||||
|
let name = ClientName::new(row.get("name"));
|
||||||
|
let secret = ClientSecret::from(row.get::<&str, &str>("secret"));
|
||||||
|
let redirect_uri = RedirectUri::new(row.get("redirect_uri"));
|
||||||
|
|
||||||
|
Ok(Some(Client::new(id, user_id, name, secret, redirect_uri)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_authorization_code(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
code_challenge: String,
|
||||||
|
code_challenge_method: CodeChallengeMethod,
|
||||||
|
) -> Result<AuthorizationCode, anyhow::Error> {
|
||||||
|
let mut tx = self
|
||||||
|
.pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.context("Failed to start sql transaction")?;
|
||||||
|
|
||||||
|
let code = AuthorizationCode::new();
|
||||||
|
|
||||||
|
let query = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO authorization_code (code, user_id, client_id, code_challenge, code_challenge_method)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(code.to_string())
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(client_id)
|
||||||
|
.bind(code_challenge)
|
||||||
|
.bind(code_challenge_method.to_string());
|
||||||
|
|
||||||
|
tx.execute(query)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CreateAuthorizationCodeError::Unknown(e.into()))?;
|
||||||
|
|
||||||
|
let query = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO authorized_clients (user_id, client_id)
|
||||||
|
VALUES ($1, $2) ON CONFLICT DO NOTHING
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(client_id);
|
||||||
|
|
||||||
|
tx.execute(query)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CreateAuthorizationCodeError::Unknown(e.into()))?;
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.await
|
||||||
|
.context("failed to commit SQL transaction")?;
|
||||||
|
|
||||||
|
Ok(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_authorized_client(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
) -> Result<bool, anyhow::Error> {
|
||||||
|
let query = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM authorized_clients WHERE user_id = $1 AND client_id = $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(client_id);
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.pool
|
||||||
|
.fetch_optional(query)
|
||||||
|
.await
|
||||||
|
.context("failed to execute SQL transaction")?
|
||||||
|
.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_token_subject(
|
||||||
|
&self,
|
||||||
|
code: AuthorizationCode,
|
||||||
|
) -> Result<Option<TokenSubject>, anyhow::Error> {
|
||||||
|
let query = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
client_id,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method
|
||||||
|
FROM authorization_code WHERE code = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(code.to_string());
|
||||||
|
|
||||||
|
let Some(row) = self
|
||||||
|
.pool
|
||||||
|
.fetch_optional(query)
|
||||||
|
.await
|
||||||
|
.context("failed to execute SQL transaction")?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id: uuid::Uuid = row.get("user_id");
|
||||||
|
let client_id: uuid::Uuid = row.get("client_id");
|
||||||
|
|
||||||
|
let code_challenge: String = row.get("code_challenge");
|
||||||
|
let code_challenge_method: CodeChallengeMethod = row
|
||||||
|
.get::<String, &str>("code_challenge_method")
|
||||||
|
.try_into()?;
|
||||||
|
|
||||||
|
Ok(Some(TokenSubject::new(
|
||||||
|
user_id,
|
||||||
|
client_id,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_token(&self, code: AuthorizationCode) -> Result<(), anyhow::Error> {
|
||||||
|
let mut tx = self
|
||||||
|
.pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.context("Failed to start sql transaction")?;
|
||||||
|
|
||||||
|
let query =
|
||||||
|
sqlx::query("DELETE FROM authorization_code WHERE code = $1").bind(code.to_string());
|
||||||
|
|
||||||
|
tx.execute(query)
|
||||||
|
.await
|
||||||
|
.context("failed to execute SQL transaction")?;
|
||||||
|
tx.commit()
|
||||||
|
.await
|
||||||
|
.context("failed to commit SQL transaction")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -216,7 +216,7 @@ impl UserRepository for Postgres {
|
|||||||
|
|
||||||
let id = row.get("user_id");
|
let id = row.get("user_id");
|
||||||
|
|
||||||
Ok(self.find_user_by_id(id).await?)
|
self.find_user_by_id(id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_user_by_password_reset_token(
|
async fn find_user_by_password_reset_token(
|
||||||
@@ -244,7 +244,7 @@ impl UserRepository for Postgres {
|
|||||||
|
|
||||||
let id = row.get("user_id");
|
let id = row.get("user_id");
|
||||||
|
|
||||||
Ok(self.find_user_by_id(id).await?)
|
self.find_user_by_id(id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_user(
|
async fn update_user(
|
||||||
@@ -295,10 +295,10 @@ impl UserRepository for Postgres {
|
|||||||
Ok((
|
Ok((
|
||||||
ent.clone(),
|
ent.clone(),
|
||||||
User::new(
|
User::new(
|
||||||
ent.id().clone(),
|
ent.id(),
|
||||||
new_email.clone(),
|
new_email.clone(),
|
||||||
new_password.clone(),
|
new_password.clone(),
|
||||||
new_verified.clone(),
|
*new_verified,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@@ -1,50 +0,0 @@
|
|||||||
use lettre::AsyncTransport;
|
|
||||||
|
|
||||||
use crate::domain::api::ports::UserNotifier;
|
|
||||||
|
|
||||||
use crate::domain::api::models::user::*;
|
|
||||||
|
|
||||||
use super::DangerousLettre;
|
|
||||||
|
|
||||||
impl UserNotifier for DangerousLettre {
|
|
||||||
async fn user_created(&self, user: &User, token: &ActivationToken) {
|
|
||||||
let mut context = tera::Context::new();
|
|
||||||
|
|
||||||
let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env
|
|
||||||
|
|
||||||
context.insert("activate_url", &url);
|
|
||||||
|
|
||||||
let to = format!("{}", user.email()).parse().unwrap();
|
|
||||||
|
|
||||||
let message = self
|
|
||||||
.template("user/created", "Welcome to AVAM!", &context, to)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Err(e) = self.mailer.send(message).await {
|
|
||||||
eprintln!("{:#?}", e);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn forgot_password(&self, user: &User, token: &ForgotPasswordToken) {
|
|
||||||
let mut context = tera::Context::new();
|
|
||||||
|
|
||||||
let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env
|
|
||||||
|
|
||||||
context.insert("reset_url", &url);
|
|
||||||
|
|
||||||
let to = format!("{}", user.email()).parse().unwrap();
|
|
||||||
|
|
||||||
let message = self
|
|
||||||
.template(
|
|
||||||
"user/password_reset",
|
|
||||||
"Password reset request",
|
|
||||||
&context,
|
|
||||||
to,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Err(e) = self.mailer.send(message).await {
|
|
||||||
eprintln!("{:#?}", e);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user