2026-09-03 21:50:32 +02:00
|
|
|
use axum::Router;
|
2026-09-04 17:33:37 +02:00
|
|
|
use axum::extract::State;
|
2026-09-08 16:39:50 +02:00
|
|
|
use axum::response::{Html, IntoResponse, Response};
|
2026-09-04 17:33:37 +02:00
|
|
|
use axum::routing::{get, post};
|
2026-09-03 21:50:32 +02:00
|
|
|
use axum::serve::Listener as AxumListener;
|
2026-09-04 17:33:37 +02:00
|
|
|
use axum_extra::extract::cookie::CookieJar;
|
|
|
|
|
use http::StatusCode;
|
|
|
|
|
use minijinja::{Environment, context, path_loader};
|
|
|
|
|
use static_serve::embed_assets;
|
2026-09-03 21:50:32 +02:00
|
|
|
|
2026-09-08 16:39:50 +02:00
|
|
|
use crate::db::{Database, DatabaseInterface, Operation};
|
2026-09-03 21:50:32 +02:00
|
|
|
use crate::listener::{Listener, ListenerKind};
|
|
|
|
|
use crate::stream::{AbstractSocketAddr, AbstractStreamKind};
|
|
|
|
|
|
2026-09-04 17:33:37 +02:00
|
|
|
mod login;
|
|
|
|
|
mod logout;
|
|
|
|
|
mod session;
|
|
|
|
|
use session::HttpSessionManager;
|
|
|
|
|
|
2026-09-03 21:50:32 +02:00
|
|
|
impl AxumListener for Listener {
|
|
|
|
|
type Io = AbstractStreamKind;
|
|
|
|
|
type Addr = AbstractSocketAddr;
|
|
|
|
|
|
|
|
|
|
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
|
|
|
|
|
loop {
|
|
|
|
|
let res = match &self.kind {
|
|
|
|
|
ListenerKind::Tcp(l) => l
|
|
|
|
|
.accept()
|
|
|
|
|
.await
|
|
|
|
|
.map(|(stream, remote_addr)| (stream.into(), remote_addr.into())),
|
|
|
|
|
ListenerKind::Uds(l) => l
|
|
|
|
|
.accept()
|
|
|
|
|
.await
|
|
|
|
|
.map(|(stream, remote_addr)| (stream.into(), remote_addr.into())),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match res {
|
|
|
|
|
Ok((stream, remote_addr)) => return (stream, remote_addr),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Here the error could be fatal, or could simply be that a client aborted the connected,
|
|
|
|
|
// in which case we don't want to crash the server, simply skip this client connection.
|
|
|
|
|
// https://doc.rust-lang.org/stable/std/net/struct.TcpListener.html#errors
|
|
|
|
|
match e.kind() {
|
|
|
|
|
std::io::ErrorKind::ConnectionAborted => {}
|
|
|
|
|
_ => panic!("Unrecoverable HTTP client connection error: {e}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn local_addr(&self) -> std::io::Result<Self::Addr> {
|
|
|
|
|
match &self.kind {
|
|
|
|
|
ListenerKind::Tcp(l) => l.local_addr().map(Into::into),
|
|
|
|
|
ListenerKind::Uds(l) => l.local_addr().map(Into::into),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 17:33:37 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct HttpState<D: DatabaseInterface> {
|
|
|
|
|
pub db: Database<D>,
|
|
|
|
|
pub sessions: HttpSessionManager,
|
|
|
|
|
pub templates: Environment<'static>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<D: DatabaseInterface> HttpState<D> {
|
|
|
|
|
pub fn new(db: Database<D>) -> Self {
|
|
|
|
|
let mut templates = Environment::new();
|
|
|
|
|
templates.set_loader(path_loader("templates"));
|
|
|
|
|
Self {
|
|
|
|
|
db,
|
|
|
|
|
sessions: HttpSessionManager::new(),
|
|
|
|
|
templates,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 21:50:32 +02:00
|
|
|
pub async fn http_listen<D: DatabaseInterface>(listener: Listener, db: Database<D>) {
|
2026-09-04 17:33:37 +02:00
|
|
|
embed_assets!("assets");
|
|
|
|
|
let app = Router::new()
|
|
|
|
|
.nest("/assets", static_router())
|
|
|
|
|
.route("/", get(home))
|
|
|
|
|
.route("/login", get(home))
|
|
|
|
|
.route("/login", post(login::post_login))
|
|
|
|
|
.route("/logout", get(logout::logout))
|
|
|
|
|
.with_state(HttpState::new(db));
|
2026-09-03 21:50:32 +02:00
|
|
|
|
|
|
|
|
axum::serve(listener, app).await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 17:33:37 +02:00
|
|
|
pub async fn home<D: DatabaseInterface>(
|
|
|
|
|
State(state): State<HttpState<D>>,
|
|
|
|
|
cookies: CookieJar,
|
2026-09-08 16:39:50 +02:00
|
|
|
) -> Response {
|
2026-09-04 17:33:37 +02:00
|
|
|
if let Some(session) = state.sessions.get_session(&cookies) {
|
2026-09-08 16:39:50 +02:00
|
|
|
// When the user has no domain (service admin) list all domains
|
|
|
|
|
let op = Operation::ListUsers(session.user.domain.clone());
|
|
|
|
|
let other_users = if session.user.can_perform(&op) {
|
|
|
|
|
match state.db.list_users(session.user.domain.clone()).await {
|
|
|
|
|
Ok(other_users) => other_users,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return format!("Database error: {e}").into_response();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Found {} users on domain {:?}",
|
|
|
|
|
other_users.len(),
|
|
|
|
|
session.user.domain
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let ctx = context! {
|
|
|
|
|
user => session.user,
|
|
|
|
|
other_users,
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-04 17:33:37 +02:00
|
|
|
let page = state
|
|
|
|
|
.templates
|
|
|
|
|
.get_template("home.html")
|
|
|
|
|
.unwrap()
|
2026-09-08 16:39:50 +02:00
|
|
|
.render(ctx)
|
2026-09-04 17:33:37 +02:00
|
|
|
.unwrap();
|
2026-09-08 16:39:50 +02:00
|
|
|
(StatusCode::OK, Html(page)).into_response()
|
2026-09-04 17:33:37 +02:00
|
|
|
} else {
|
|
|
|
|
login::login_page(State(state), None).await
|
|
|
|
|
}
|
2026-09-03 21:50:32 +02:00
|
|
|
}
|