diff --git a/Cargo.lock b/Cargo.lock index 545982a..0b55b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -119,6 +120,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "base64" version = "0.22.1" diff --git a/Cargo.toml b/Cargo.toml index 903b294..2a473bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] argh = "0.1.19" -axum = { version = "0.8.9", optional = true } +axum = { version = "0.8.9", optional = true, features = ["macros"] } axum-extra = { version = "0.12.6", features = ["cookie"], optional = true } camino = "1.2.5" dn_escape = { path = "vendor/dn_escape" } diff --git a/src/db/common.rs b/src/db/common.rs index 4f278b8..d75ba10 100644 --- a/src/db/common.rs +++ b/src/db/common.rs @@ -3,7 +3,7 @@ use tokio::sync::RwLock; use std::sync::Arc; use crate::db::error::BoxedError; -use crate::db::{DatabaseInterface, UserRef}; +use crate::db::{DatabaseInterface, Domain, User, UserRef}; #[derive(Clone, Debug)] pub struct Database { @@ -36,4 +36,13 @@ impl Database { tracing::debug!("Comparing {} and {}", user.password, password); Ok(user.password == password) } + + pub async fn domains_user_can_see(&self, user: &User) -> Result, BoxedError> { + Ok(self + .list_all_domains() + .await? + .into_iter() + .filter(|d| user.role.can_see_domain(&d.name)) + .collect()) + } } diff --git a/src/db/domain.rs b/src/db/domain.rs index 6cca261..59a1a94 100644 --- a/src/db/domain.rs +++ b/src/db/domain.rs @@ -1,4 +1,6 @@ -#[derive(Clone, Debug, Default)] +use serde::Serialize; + +#[derive(Clone, Debug, Default, Serialize)] pub struct Domain { pub name: String, } diff --git a/src/db/interface.rs b/src/db/interface.rs index e5d58ec..42154e7 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -36,6 +36,10 @@ impl DatabaseInterface for Database { .await } + async fn list_all_domains(&self) -> Result, BoxedError> { + self.inner.read().await.list_all_domains().await + } + async fn list_all_users(&self) -> Result, BoxedError> { self.inner.read().await.list_all_users().await } @@ -67,8 +71,9 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static { current_user: &User, ) -> Result, BoxedError>; + fn list_all_domains(&self) -> impl Future, BoxedError>> + Send; #[expect(unused)] - fn list_all_users(&self) -> impl Future, BoxedError>>; + fn list_all_users(&self) -> impl Future, BoxedError>> + Send; /// List users on a specific domain. /// diff --git a/src/db/memory.rs b/src/db/memory.rs index 1ba13dd..83c3178 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -104,6 +104,10 @@ impl DatabaseInterface for MemoryDatabase { self.create_user(new_user).await } + fn list_all_domains(&self) -> impl Future, BoxedError>> { + ready(Ok(self.domains.clone())) + } + fn list_all_users(&self) -> impl Future, BoxedError>> { ready(Ok(self.users.clone())) } diff --git a/src/db/role.rs b/src/db/role.rs index f717813..102933b 100644 --- a/src/db/role.rs +++ b/src/db/role.rs @@ -2,11 +2,12 @@ use serde::Serialize; #[derive(Clone, Debug)] pub enum Operation { + CreateDomain, CreateUser(String), ListUsers(Option), } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub enum Role { /// Can do anything Admin, @@ -28,6 +29,7 @@ pub enum Role { impl Role { pub fn can_perform(&self, operation: &Operation) -> bool { match operation { + Operation::CreateDomain => self == &Self::Admin, Operation::CreateUser(op_domain) => match self { Self::Admin => true, Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => { @@ -44,4 +46,12 @@ impl Role { }, } } + + pub fn can_see_domain(&self, domain: &str) -> bool { + match self { + Self::Admin | Self::ReadonlyAdmin => true, + Self::DomainAdmin(d) | Self::DomainModerator(d) => domain == d, + Self::User => false, + } + } } diff --git a/src/db/user.rs b/src/db/user.rs index 5a325b1..967426a 100644 --- a/src/db/user.rs +++ b/src/db/user.rs @@ -80,6 +80,10 @@ impl User { pub fn can_perform(&self, operation: &Operation) -> bool { self.role.can_perform(operation) } + + pub fn can_create_domain(&self) -> bool { + self.role.can_perform(&Operation::CreateDomain) + } } impl fmt::Display for User { diff --git a/src/http/home.rs b/src/http/home.rs new file mode 100644 index 0000000..595ac17 --- /dev/null +++ b/src/http/home.rs @@ -0,0 +1,63 @@ +use axum::extract::State; +use axum::response::{Html, IntoResponse, Response}; +use axum_extra::extract::cookie::CookieJar; +use http::StatusCode; +use minijinja::context; + +use crate::db::{DatabaseInterface, Operation}; +use crate::http::HttpState; +use crate::http::login::login_page; + +pub async fn home( + State(state): State>, + cookies: CookieJar, +) -> Response { + if let Some(session) = state.sessions.get_session(&cookies) { + // 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_domain_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 domains = match state.db.domains_user_can_see(&session.user).await { + Ok(domains) => domains, + Err(e) => { + return format!("Database error: {e}").into_response(); + } + }; + + let ctx = context! { + domains, + user => session.user, + can_create_domain => session.user.can_create_domain(), + other_users, + }; + + let page = state + .templates + .get_template("home.html") + .unwrap() + .render(ctx) + .unwrap(); + (StatusCode::OK, Html(page)).into_response() + } else { + login_page(State(state), None).await + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index bd5b2ec..1d65472 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,22 +1,19 @@ use axum::Router; -use axum::extract::State; -use axum::response::{Html, IntoResponse, Response}; use axum::routing::{get, post}; use axum::serve::Listener as AxumListener; -use axum_extra::extract::cookie::CookieJar; -use http::StatusCode; +use minijinja::Environment; #[cfg(not(feature = "embed"))] use minijinja::path_loader; -use minijinja::{Environment, context}; #[cfg(feature = "embed")] use static_serve::embed_assets; #[cfg(not(feature = "embed"))] use tower_http::services::ServeDir; -use crate::db::{Database, DatabaseInterface, Operation}; +use crate::db::{Database, DatabaseInterface}; use crate::listener::{Listener, ListenerKind}; use crate::stream::{AbstractSocketAddr, AbstractStreamKind}; +mod home; mod login; mod logout; mod session; @@ -98,56 +95,11 @@ pub async fn http_listen(listener: Listener, db: Database< #[cfg(not(feature = "embed"))] let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) }; let app = app - .route("/", get(home)) - .route("/login", get(home)) + .route("/", get(home::home)) + .route("/login", get(home::home)) .route("/login", post(login::post_login)) .route("/logout", get(logout::logout)) .with_state(HttpState::new(db)); axum::serve(listener, app).await.unwrap(); } - -pub async fn home( - State(state): State>, - cookies: CookieJar, -) -> Response { - if let Some(session) = state.sessions.get_session(&cookies) { - // 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_domain_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, - }; - - let page = state - .templates - .get_template("home.html") - .unwrap() - .render(ctx) - .unwrap(); - (StatusCode::OK, Html(page)).into_response() - } else { - login::login_page(State(state), None).await - } -} diff --git a/templates/home.html b/templates/home.html index e9f4925..15eb0da 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,6 +6,23 @@ + {% if can_create_domain %} +
+

Create domain

+
+ + +
+
+
+

Active domains you can see

+ +
+ {% endif %} {% if other_users %}

Other users you have permission to see on your own domain