add umbrella crate

This commit is contained in:
Aleksandr 2025-09-20 22:55:40 +03:00
parent d171fc723b
commit fe04530f84
17 changed files with 247 additions and 5 deletions

View file

@ -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.

8
auth/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "viendesu-auth"
version = "0.1.0"
edition = "2024"
[dependencies]
eva.workspace = true
viendesu-core.workspace = true

1
auth/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod passwords;

24
auth/src/passwords.rs Normal file
View file

@ -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()
}
}

8
captcha/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "viendesu-captcha"
version = "0.1.0"
edition = "2024"
[dependencies]
eva.workspace = true
viendesu-core.workspace = true

22
captcha/src/error.rs Normal file
View file

@ -0,0 +1,22 @@
use eva::str::ToCompactString as _;
use viendesu_core::{
errors::{Aux, Generic},
mk_error,
};
pub type CaptchaResult<T> = Result<T, CaptchaError>;
mk_error!(CaptchaError);
impl From<CaptchaError> for Aux {
fn from(value: CaptchaError) -> Self {
Self::Captcha(value.0.to_compact_string())
}
}
impl<S> From<CaptchaError> for Generic<S> {
fn from(value: CaptchaError) -> Self {
Self::Aux(value.into())
}
}

32
captcha/src/lib.rs Normal file
View file

@ -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<W: WorldMut>(
&self,
w: World<W>,
token: &Token,
) -> impl Fut<Output = CaptchaResult<()>>;
}
impl Service for bool {
async fn verify<W: WorldMut>(&self, w: World<W>, token: &Token) -> CaptchaResult<()> {
_ = w;
_ = token;
if *self {
Ok(())
} else {
bail!("invalid captcha")
}
}
}

12
email/Cargo.toml Normal file
View file

@ -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

11
email/src/error.rs Normal file
View file

@ -0,0 +1,11 @@
use viendesu_core::{errors::Aux, mk_error};
pub type MailResult<O> = Result<O, MailError>;
mk_error!(MailError);
impl From<MailError> for Aux {
fn from(value: MailError) -> Self {
Self::Mail(eva::str::format_compact!("{}", value.0))
}
}

20
email/src/lib.rs Normal file
View file

@ -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<Output = error::MailResult<()>>;
}

43
email/src/mock.rs Normal file
View file

@ -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<Letter<'static>>;
pub type Letters = HashMap<CompactString, Mailbox>;
#[derive(Debug, Clone, Default)]
pub struct Mock(Arc<Mutex<Letters>>);
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(())
}
}

1
email/src/smtp.rs Normal file
View file

@ -0,0 +1 @@
pub struct Smtp;

View file

@ -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 }

View file

@ -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<T: Types>(service: T::Service) -> axum::Router {
let scope = handler::RouterScope::root(State::<T> { service });
routes::make(scope).into_axum()

View file

@ -52,8 +52,8 @@ pub fn ok<O: IsResponse>(format: Format, ok: O) -> AxumResponse {
pub fn respond<O: IsResponse>(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);

17
viendesu/Cargo.toml Normal file
View file

@ -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 }

5
viendesu/src/lib.rs Normal file
View file

@ -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;