From fe04530f84a1e0b4a791db18196c23cd02f97563 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Sat, 20 Sep 2025 22:55:40 +0300 Subject: [PATCH] add umbrella crate --- Cargo.toml | 8 +++++- auth/Cargo.toml | 8 ++++++ auth/src/lib.rs | 1 + auth/src/passwords.rs | 24 ++++++++++++++++++ captcha/Cargo.toml | 8 ++++++ captcha/src/error.rs | 22 +++++++++++++++++ captcha/src/lib.rs | 32 ++++++++++++++++++++++++ email/Cargo.toml | 12 +++++++++ email/src/error.rs | 11 +++++++++ email/src/lib.rs | 20 +++++++++++++++ email/src/mock.rs | 43 +++++++++++++++++++++++++++++++++ email/src/smtp.rs | 1 + http/Cargo.toml | 6 ++++- http/src/server/mod.rs | 30 ++++++++++++++++++++++- http/src/server/response/mod.rs | 4 +-- viendesu/Cargo.toml | 17 +++++++++++++ viendesu/src/lib.rs | 5 ++++ 17 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 auth/Cargo.toml create mode 100644 auth/src/lib.rs create mode 100644 auth/src/passwords.rs create mode 100644 captcha/Cargo.toml create mode 100644 captcha/src/error.rs create mode 100644 captcha/src/lib.rs create mode 100644 email/Cargo.toml create mode 100644 email/src/error.rs create mode 100644 email/src/lib.rs create mode 100644 email/src/mock.rs create mode 100644 email/src/smtp.rs create mode 100644 viendesu/Cargo.toml create mode 100644 viendesu/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 7144380..7205d44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [workspace] members = [ "core/", - "http/" + "http/", + "auth/", + "captcha/", + "viendesu/", ] resolver = "3" @@ -23,6 +26,9 @@ schemars = { git = "ssh://forgejo@git.viende.su:61488/VienDesu/schemars.git", fe "hashbrown015" ] } +viendesu-captcha = { path = "captcha/" } +viendesu-auth = { path = "auth/" } +viendesu-http = { path = "http/" } viendesu-core = { path = "core/" } # TODO: upstream. diff --git a/auth/Cargo.toml b/auth/Cargo.toml new file mode 100644 index 0000000..848c676 --- /dev/null +++ b/auth/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "viendesu-auth" +version = "0.1.0" +edition = "2024" + +[dependencies] +eva.workspace = true +viendesu-core.workspace = true diff --git a/auth/src/lib.rs b/auth/src/lib.rs new file mode 100644 index 0000000..67d75c6 --- /dev/null +++ b/auth/src/lib.rs @@ -0,0 +1 @@ +pub mod passwords; diff --git a/auth/src/passwords.rs b/auth/src/passwords.rs new file mode 100644 index 0000000..9ff0fd6 --- /dev/null +++ b/auth/src/passwords.rs @@ -0,0 +1,24 @@ +use eva::auto_impl; + +use viendesu_core::types::user::PasswordHash; + +/// Passwords generation utility. +#[auto_impl(&, &mut, Arc)] +pub trait Passwords: Send + Sync { + fn verify(&self, hash: &str, cleartext: &str) -> bool; + fn make(&self, cleartext: &str) -> PasswordHash; +} + +/// Plaintext passwords, no hashing. +#[derive(Debug, Clone, Copy)] +pub struct Plaintext; + +impl Passwords for Plaintext { + fn verify(&self, hash: &str, cleartext: &str) -> bool { + hash == cleartext + } + + fn make(&self, cleartext: &str) -> PasswordHash { + cleartext.parse().unwrap() + } +} diff --git a/captcha/Cargo.toml b/captcha/Cargo.toml new file mode 100644 index 0000000..45bbd6b --- /dev/null +++ b/captcha/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "viendesu-captcha" +version = "0.1.0" +edition = "2024" + +[dependencies] +eva.workspace = true +viendesu-core.workspace = true diff --git a/captcha/src/error.rs b/captcha/src/error.rs new file mode 100644 index 0000000..4528d5c --- /dev/null +++ b/captcha/src/error.rs @@ -0,0 +1,22 @@ +use eva::str::ToCompactString as _; + +use viendesu_core::{ + errors::{Aux, Generic}, + mk_error, +}; + +pub type CaptchaResult = Result; + +mk_error!(CaptchaError); + +impl From for Aux { + fn from(value: CaptchaError) -> Self { + Self::Captcha(value.0.to_compact_string()) + } +} + +impl From for Generic { + fn from(value: CaptchaError) -> Self { + Self::Aux(value.into()) + } +} diff --git a/captcha/src/lib.rs b/captcha/src/lib.rs new file mode 100644 index 0000000..6980e51 --- /dev/null +++ b/captcha/src/lib.rs @@ -0,0 +1,32 @@ +use eva::fut::Fut; + +use viendesu_core::{ + bail, + types::captcha::Token, + world::{World, WorldMut}, +}; + +use self::error::CaptchaResult; + +pub mod error; + +pub trait Service: Send + Sync { + fn verify( + &self, + w: World, + token: &Token, + ) -> impl Fut>; +} + +impl Service for bool { + async fn verify(&self, w: World, token: &Token) -> CaptchaResult<()> { + _ = w; + _ = token; + + if *self { + Ok(()) + } else { + bail!("invalid captcha") + } + } +} diff --git a/email/Cargo.toml b/email/Cargo.toml new file mode 100644 index 0000000..be87473 --- /dev/null +++ b/email/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "viendesu-email" +version = "0.1.0" +edition = "2024" + +[features] +default = ["smtp"] +smtp = [] + +[dependencies] +viendesu-core.workspace = true +eva.workspace = true diff --git a/email/src/error.rs b/email/src/error.rs new file mode 100644 index 0000000..85f2e7c --- /dev/null +++ b/email/src/error.rs @@ -0,0 +1,11 @@ +use viendesu_core::{errors::Aux, mk_error}; + +pub type MailResult = Result; + +mk_error!(MailError); + +impl From for Aux { + fn from(value: MailError) -> Self { + Self::Mail(eva::str::format_compact!("{}", value.0)) + } +} diff --git a/email/src/lib.rs b/email/src/lib.rs new file mode 100644 index 0000000..dacbbfc --- /dev/null +++ b/email/src/lib.rs @@ -0,0 +1,20 @@ +use eva::{data, fut::Fut}; + +use std::borrow::Cow; + +pub mod mock; +pub mod smtp; + +pub mod error; + +#[data] +pub struct Letter<'a> { + pub subject: Cow<'a, str>, + pub contents: Cow<'a, str>, + pub content_type: Cow<'a, str>, +} + +#[eva::auto_impl(&, &mut, Arc, Box)] +pub trait Mailer: Send + Sync { + fn send(&self, dst: &str, letter: Letter<'_>) -> impl Fut>; +} diff --git a/email/src/mock.rs b/email/src/mock.rs new file mode 100644 index 0000000..e937a05 --- /dev/null +++ b/email/src/mock.rs @@ -0,0 +1,43 @@ +use std::{ + borrow::Cow, + sync::{Arc, Mutex}, +}; + +use eva::{ + collections::HashMap, + str::{CompactString, ToCompactString}, +}; + +use crate::{Letter, Mailer, error::MailResult}; + +pub type Mailbox = Vec>; +pub type Letters = HashMap; + +#[derive(Debug, Clone, Default)] +pub struct Mock(Arc>); + +impl Mock { + pub fn collect_letters(&self) -> Letters { + std::mem::take(&mut *self.0.lock().unwrap()) + } +} + +fn owned(c: Cow<'_, str>) -> Cow<'static, str> { + Cow::Owned((*c).to_owned()) +} + +impl Mailer for Mock { + async fn send(&self, dst: &str, letter: Letter<'_>) -> MailResult<()> { + let dst = dst.to_compact_string(); + let mut this = self.0.lock().unwrap(); + let mailbox = this.entry(dst).or_default(); + + mailbox.push(Letter { + subject: owned(letter.subject), + contents: owned(letter.contents), + content_type: owned(letter.content_type), + }); + + Ok(()) + } +} diff --git a/email/src/smtp.rs b/email/src/smtp.rs new file mode 100644 index 0000000..5f6bc00 --- /dev/null +++ b/email/src/smtp.rs @@ -0,0 +1 @@ +pub struct Smtp; diff --git a/http/Cargo.toml b/http/Cargo.toml index 32461cf..8e8ac41 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -4,13 +4,15 @@ version = "0.1.0" edition = "2024" [features] -default = [] +default = ["client", "server"] client = ["dep:reqwest"] server = [ "dep:axum", "dep:http-body-util", "dep:tower-http", "dep:serde_urlencoded", + "eva/tokio", + "tokio/net" ] [dependencies] @@ -30,4 +32,6 @@ http-body-util = { version = "0.1.3", optional = true } tower-http = { optional = true, version = "0.6.6", default-features = false, features = ["limit"] } serde_urlencoded = { version = "0.7.1", optional = true } +tokio = { workspace = true, optional = true } + reqwest = { optional = true, version = "0.12.23", default-features = false } diff --git a/http/src/server/mod.rs b/http/src/server/mod.rs index 16dc381..18d8a2c 100644 --- a/http/src/server/mod.rs +++ b/http/src/server/mod.rs @@ -1,10 +1,13 @@ -use eva::perfect_derive; +use eva::{perfect_derive, supervisor::SlaveRx}; +use eyre::Context; use viendesu_core::{ errors::AuxResult, service::{Service, SessionMaker, SessionOf}, }; +use tokio::net; + pub mod config; mod context; @@ -18,6 +21,31 @@ pub trait Types: Send + Sync + 'static { type Service: Service + Clone; } +pub async fn serve( + rx: SlaveRx, + config: config::Config, + router: axum::Router, +) -> eyre::Result<()> { + // TODO: use it. + _ = rx; + let config::Config { unencrypted, ssl: _ } = config; + let unencrypted = unencrypted.expect("SSL-only currently is not supported"); + + if !unencrypted.enable { + return Ok(()); + } + + let listener = net::TcpListener::bind(unencrypted.listen) + .await + .wrap_err("failed to bind address")?; + + axum::serve(listener, router) + .await + .wrap_err("failed to serve")?; + + Ok(()) +} + pub fn make_router(service: T::Service) -> axum::Router { let scope = handler::RouterScope::root(State:: { service }); routes::make(scope).into_axum() diff --git a/http/src/server/response/mod.rs b/http/src/server/response/mod.rs index 1431a5d..11ebfc9 100644 --- a/http/src/server/response/mod.rs +++ b/http/src/server/response/mod.rs @@ -52,8 +52,8 @@ pub fn ok(format: Format, ok: O) -> AxumResponse { pub fn respond(format: Format, res: O) -> AxumResponse { let status_code = res.status_code(); let headers = [ - header!("Server" => "Kurisu-desu"), - header!("Content-Type" => format.mime_type()), + header!("server" => "Kurisu-desu"), + header!("content-type" => format.mime_type()), ]; let mut body = Vec::with_capacity(128); diff --git a/viendesu/Cargo.toml b/viendesu/Cargo.toml new file mode 100644 index 0000000..df768fd --- /dev/null +++ b/viendesu/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "viendesu" +version = "0.1.0" +edition = "2024" + +[features] +default = [] + +tokio = ["viendesu-core/tokio"] +http-client = ["viendesu-http/client"] +http-server = ["viendesu-http/server"] + +[dependencies] +viendesu-core = { workspace = true } +viendesu-http = { workspace = true } +viendesu-auth = { workspace = true } +viendesu-captcha = { workspace = true } diff --git a/viendesu/src/lib.rs b/viendesu/src/lib.rs new file mode 100644 index 0000000..e20bbcb --- /dev/null +++ b/viendesu/src/lib.rs @@ -0,0 +1,5 @@ +pub use viendesu_core::*; + +pub use viendesu_auth as auth; +pub use viendesu_captcha as captcha; +pub use viendesu_http as http;