Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28c584b26 | |||
| 78df25f150 | |||
| ae5d466e99 | |||
| 38700ee4cb | |||
| 8a7e644960 | |||
| 58a8c375f0 | |||
| f1a2721cbe |
17 changed files with 444 additions and 91 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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<D: DatabaseInterface> {
|
||||
|
|
@ -36,4 +36,13 @@ impl<D: DatabaseInterface> Database<D> {
|
|||
tracing::debug!("Comparing {} and {}", user.password, password);
|
||||
Ok(user.password == password)
|
||||
}
|
||||
|
||||
pub async fn domains_user_can_see(&self, user: &User) -> Result<Vec<Domain>, BoxedError> {
|
||||
Ok(self
|
||||
.list_all_domains()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|d| user.can_see_domain(&d.name))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/db/domain.rs
Normal file
6
src/db/domain.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Domain {
|
||||
pub name: String,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ pub type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum UserCreationError {
|
||||
DomainNotFound(String),
|
||||
UserAlreadyExists(UserRef),
|
||||
Permissions,
|
||||
}
|
||||
|
|
@ -13,6 +14,7 @@ pub enum UserCreationError {
|
|||
impl fmt::Display for UserCreationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::DomainNotFound(domain) => write!(f, "No domain {domain} to create user in"),
|
||||
Self::UserAlreadyExists(user) => write!(f, "User already exists: {user}"),
|
||||
Self::Permissions => write!(f, "You do not have permissions to create this user"),
|
||||
}
|
||||
|
|
@ -20,3 +22,25 @@ impl fmt::Display for UserCreationError {
|
|||
}
|
||||
|
||||
impl std::error::Error for UserCreationError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DomainCreationError {
|
||||
DomainAlreadyExists(String),
|
||||
InvalidDomain(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for DomainCreationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::DomainAlreadyExists(domain) => {
|
||||
write!(f, "Cannot create domain {domain} because it already exists")
|
||||
}
|
||||
Self::InvalidDomain(domain) => write!(
|
||||
f,
|
||||
"Cannot create domain `{domain}` because it's not considered a valid domain"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DomainCreationError {}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
use crate::db::error::{BoxedError, UserCreationError};
|
||||
use crate::db::{Database, User, UserRef};
|
||||
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
||||
use crate::db::{Database, Domain, User, UserRef};
|
||||
|
||||
impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
|
||||
async fn create_domain(
|
||||
&mut self,
|
||||
domain: &str,
|
||||
) -> Result<Result<(), DomainCreationError>, BoxedError> {
|
||||
self.inner.write().await.create_domain(domain).await
|
||||
}
|
||||
|
||||
async fn get_domain(&self, domain: &str) -> Result<Option<Domain>, BoxedError> {
|
||||
self.inner.read().await.get_domain(domain).await
|
||||
}
|
||||
|
||||
async fn get_user(&self, user: &UserRef) -> Result<Option<User>, BoxedError> {
|
||||
self.inner.read().await.get_user(user).await
|
||||
}
|
||||
|
|
@ -25,12 +36,29 @@ impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> {
|
||||
self.inner.read().await.list_users(domain).await
|
||||
async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError> {
|
||||
self.inner.read().await.list_all_domains().await
|
||||
}
|
||||
|
||||
async fn list_all_users(&self) -> Result<Vec<User>, BoxedError> {
|
||||
self.inner.read().await.list_all_users().await
|
||||
}
|
||||
|
||||
async fn list_domain_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> {
|
||||
self.inner.read().await.list_domain_users(domain).await
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DatabaseInterface: Clone + Send + Sync + 'static {
|
||||
fn create_domain(
|
||||
&mut self,
|
||||
domain: &str,
|
||||
) -> impl Future<Output = Result<Result<(), DomainCreationError>, BoxedError>> + Send;
|
||||
fn get_domain(
|
||||
&self,
|
||||
domain: &str,
|
||||
) -> impl Future<Output = Result<Option<Domain>, BoxedError>> + Send;
|
||||
|
||||
fn get_user(
|
||||
&self,
|
||||
user: &UserRef,
|
||||
|
|
@ -39,13 +67,20 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
|
|||
&mut self,
|
||||
user: User,
|
||||
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
||||
#[expect(unused)]
|
||||
async fn try_create_user(
|
||||
fn try_create_user(
|
||||
&mut self,
|
||||
new_user: User,
|
||||
current_user: &User,
|
||||
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
||||
fn list_users(
|
||||
) -> impl Future<Output = Result<Result<(), UserCreationError>, BoxedError>> + Send;
|
||||
|
||||
fn list_all_domains(&self) -> impl Future<Output = Result<Vec<Domain>, BoxedError>> + Send;
|
||||
#[expect(unused)]
|
||||
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
||||
|
||||
/// List users on a specific domain.
|
||||
///
|
||||
/// A `None` domain requested lists global service users.
|
||||
fn list_domain_users(
|
||||
&self,
|
||||
domain: Option<String>,
|
||||
) -> impl std::future::Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
use std::future::{Future, ready};
|
||||
|
||||
use crate::db::error::{BoxedError, UserCreationError};
|
||||
use crate::db::{Database, DatabaseInterface, Group, Operation, User, UserRef};
|
||||
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
||||
use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MemoryDatabase {
|
||||
pub users: Vec<User>,
|
||||
// We store data in tables like in SQL
|
||||
pub domains: Vec<Domain>,
|
||||
#[expect(unused)]
|
||||
pub groups: Vec<Group>,
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl MemoryDatabase {
|
||||
|
|
@ -17,17 +19,46 @@ impl MemoryDatabase {
|
|||
}
|
||||
|
||||
impl DatabaseInterface for MemoryDatabase {
|
||||
fn create_domain(
|
||||
&mut self,
|
||||
domain: &str,
|
||||
) -> impl Future<Output = Result<Result<(), DomainCreationError>, BoxedError>> {
|
||||
if domain.is_empty() {
|
||||
return ready(Ok(Err(DomainCreationError::InvalidDomain(
|
||||
domain.to_string(),
|
||||
))));
|
||||
}
|
||||
|
||||
if self.domains.iter().find(|d| d.name == domain).is_some() {
|
||||
return ready(Ok(Err(DomainCreationError::DomainAlreadyExists(
|
||||
domain.to_string(),
|
||||
))));
|
||||
}
|
||||
|
||||
self.domains.push(Domain {
|
||||
name: domain.to_string(),
|
||||
});
|
||||
|
||||
ready(Ok(Ok(())))
|
||||
}
|
||||
|
||||
fn get_domain(&self, domain: &str) -> impl Future<Output = Result<Option<Domain>, BoxedError>> {
|
||||
let Some(domain) = self.domains.iter().find(|d| d.name == domain) else {
|
||||
return ready(Ok(None));
|
||||
};
|
||||
|
||||
ready(Ok(Some(domain.clone())))
|
||||
}
|
||||
|
||||
fn get_user(
|
||||
&self,
|
||||
req_user: &UserRef,
|
||||
) -> impl Future<Output = Result<Option<User>, BoxedError>> {
|
||||
for user in &self.users {
|
||||
if user.username == req_user.username && user.domain == req_user.domain {
|
||||
return ready(Ok(Some(user.clone())));
|
||||
}
|
||||
}
|
||||
|
||||
ready(Ok(None))
|
||||
ready(Ok(self
|
||||
.users
|
||||
.iter()
|
||||
.find(|u| u.username == req_user.username && u.domain == req_user.domain)
|
||||
.cloned()))
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
|
|
@ -35,10 +66,18 @@ impl DatabaseInterface for MemoryDatabase {
|
|||
user: User,
|
||||
) -> Result<Result<(), UserCreationError>, BoxedError> {
|
||||
let user_ref = user.user_ref();
|
||||
|
||||
if self.get_user(&user_ref).await?.is_some() {
|
||||
return Ok(Err(UserCreationError::UserAlreadyExists(user_ref)));
|
||||
}
|
||||
|
||||
// If a domain is requested (i.e. not a global user), make sure the domain exists
|
||||
if let Some(req_domain) = &user.domain
|
||||
&& self.get_domain(req_domain).await?.is_none()
|
||||
{
|
||||
return Ok(Err(UserCreationError::DomainNotFound(req_domain.clone())));
|
||||
}
|
||||
|
||||
self.users.push(user);
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
|
@ -52,7 +91,6 @@ impl DatabaseInterface for MemoryDatabase {
|
|||
//
|
||||
// TODO: for now we don't allow creating service users manually
|
||||
// so we assume there's a domain provided
|
||||
// TODO: restrict user creation on non-declared domains
|
||||
let Some(new_user_domain) = &new_user.domain else {
|
||||
return Ok(Err(UserCreationError::Permissions));
|
||||
};
|
||||
|
|
@ -65,19 +103,23 @@ impl DatabaseInterface for MemoryDatabase {
|
|||
self.create_user(new_user).await
|
||||
}
|
||||
|
||||
fn list_users(
|
||||
fn list_all_domains(&self) -> impl Future<Output = Result<Vec<Domain>, BoxedError>> {
|
||||
ready(Ok(self.domains.clone()))
|
||||
}
|
||||
|
||||
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
||||
ready(Ok(self.users.clone()))
|
||||
}
|
||||
|
||||
fn list_domain_users(
|
||||
&self,
|
||||
domain: Option<String>,
|
||||
) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
||||
let users = if let Some(domain) = domain {
|
||||
self.users
|
||||
.iter()
|
||||
.filter(|user| user.domain.as_ref() == Some(&domain))
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
self.users.clone()
|
||||
};
|
||||
ready(Ok(users))
|
||||
ready(Ok(self
|
||||
.users
|
||||
.iter()
|
||||
.filter(|u| u.domain == domain)
|
||||
.cloned()
|
||||
.collect()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
mod common;
|
||||
pub use common::Database;
|
||||
mod domain;
|
||||
pub use domain::Domain;
|
||||
pub mod error;
|
||||
mod group;
|
||||
pub use group::Group;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ use serde::Serialize;
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Operation {
|
||||
CreateDomain,
|
||||
CreateUser(String),
|
||||
ListUsers(Option<String>),
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,16 @@ 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)
|
||||
}
|
||||
|
||||
pub fn can_see_domain(&self, domain: &str) -> bool {
|
||||
let allowed = self.role.can_see_domain(domain);
|
||||
tracing::debug!("{} can see domain {}: {}", self.mail, domain, allowed);
|
||||
allowed
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for User {
|
||||
|
|
|
|||
81
src/http/domain.rs
Normal file
81
src/http/domain.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use axum::extract::{Form, Path, State};
|
||||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use http::StatusCode;
|
||||
use minijinja::context;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::db::{DatabaseInterface, Operation};
|
||||
use crate::http::HttpState;
|
||||
use crate::http::login::login_page;
|
||||
|
||||
pub async fn get_domain<D: DatabaseInterface>(
|
||||
State(state): State<HttpState<D>>,
|
||||
cookies: CookieJar,
|
||||
Path(domain): Path<String>,
|
||||
) -> Response {
|
||||
let Some(session) = state.sessions.get_session(&cookies) else {
|
||||
return login_page(State(state), None).await.into_response();
|
||||
};
|
||||
|
||||
let domain = match state.db.get_domain(&domain).await {
|
||||
Ok(Some(domain)) => domain,
|
||||
Ok(None) => return format!("Domain not found: {domain}").into_response(),
|
||||
Err(e) => {
|
||||
return format!("Database error: {e}").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let domain_users = match state.db.list_domain_users(Some(domain.name.clone())).await {
|
||||
Ok(users) => users,
|
||||
Err(e) => {
|
||||
return format!("Database error: {e}").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Redundant because someone who can see the domain admin page for the moment
|
||||
// always can create accounts.
|
||||
let op = Operation::CreateUser(domain.name.clone());
|
||||
let can_create_user = session.user.can_perform(&op);
|
||||
|
||||
let ctx = context! {
|
||||
can_create_user,
|
||||
domain,
|
||||
user => session.user,
|
||||
users => domain_users,
|
||||
};
|
||||
|
||||
let page = state
|
||||
.templates
|
||||
.get_template("domain.html")
|
||||
.unwrap()
|
||||
.render(ctx)
|
||||
.unwrap();
|
||||
(StatusCode::OK, Html(page)).into_response()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct DomainCreationForm {
|
||||
domainname: String,
|
||||
}
|
||||
|
||||
pub async fn create_domain<D: DatabaseInterface>(
|
||||
State(mut state): State<HttpState<D>>,
|
||||
cookies: CookieJar,
|
||||
Form(form): Form<DomainCreationForm>,
|
||||
) -> Response {
|
||||
let Some(session) = state.sessions.get_session(&cookies) else {
|
||||
return login_page(State(state), None).await.into_response();
|
||||
};
|
||||
|
||||
let op = Operation::CreateDomain;
|
||||
if !session.user.can_perform(&op) {
|
||||
return "Not authorized to create a new domain".into_response();
|
||||
}
|
||||
|
||||
match state.db.create_domain(&form.domainname).await {
|
||||
Ok(Ok(())) => Redirect::to(&format!("/domain/{}", form.domainname)).into_response(),
|
||||
Ok(Err(e)) => format!("Failed to create domain {}: {}", form.domainname, e).into_response(),
|
||||
Err(e) => format!("Database error: {e}").into_response(),
|
||||
}
|
||||
}
|
||||
63
src/http/home.rs
Normal file
63
src/http/home.rs
Normal file
|
|
@ -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<D: DatabaseInterface>(
|
||||
State(state): State<HttpState<D>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,25 @@
|
|||
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 domain;
|
||||
mod home;
|
||||
mod login;
|
||||
mod logout;
|
||||
mod session;
|
||||
use session::HttpSessionManager;
|
||||
mod user;
|
||||
|
||||
impl AxumListener for Listener {
|
||||
type Io = AbstractStreamKind;
|
||||
|
|
@ -98,52 +97,14 @@ pub async fn http_listen<D: DatabaseInterface>(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))
|
||||
.route("/domain/{domain}", get(domain::get_domain))
|
||||
.route("/domain", post(domain::create_domain))
|
||||
.route("/user", post(user::create_user))
|
||||
.with_state(HttpState::new(db));
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn home<D: DatabaseInterface>(
|
||||
State(state): State<HttpState<D>>,
|
||||
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_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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
src/http/user.rs
Normal file
49
src/http/user.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use axum::extract::{Form, State};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::db::{DatabaseInterface, Role, User};
|
||||
use crate::http::HttpState;
|
||||
use crate::http::login::login_page;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UserCreationForm {
|
||||
pub username: String,
|
||||
pub domain: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn create_user<D: DatabaseInterface>(
|
||||
State(mut state): State<HttpState<D>>,
|
||||
cookies: CookieJar,
|
||||
Form(form): Form<UserCreationForm>,
|
||||
) -> Response {
|
||||
let Some(session) = state.sessions.get_session(&cookies) else {
|
||||
return login_page(State(state), None).await.into_response();
|
||||
};
|
||||
|
||||
// let domain = form.domain;
|
||||
// let op = Operation::CreateUser(domain.clone());
|
||||
// if !session.user.can_perform(&op) {
|
||||
// return format!("Not authorized to create a new user on domain {domain}").into_response()
|
||||
// }
|
||||
|
||||
let new_user = User {
|
||||
mail: format!("{}@{}", form.username, form.domain),
|
||||
username: form.username.clone(),
|
||||
domain: Some(form.domain.clone()),
|
||||
password: form.password,
|
||||
role: Role::User,
|
||||
};
|
||||
|
||||
match state.db.try_create_user(new_user, &session.user).await {
|
||||
Ok(Ok(())) => Redirect::to(&format!("/domain/{}", form.domain)).into_response(),
|
||||
Ok(Err(e)) => format!(
|
||||
"Failed to create user {} on domain {}: {}",
|
||||
form.username, form.domain, e
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => format!("Database error: {e}").into_response(),
|
||||
}
|
||||
}
|
||||
18
src/main.rs
18
src/main.rs
|
|
@ -29,22 +29,24 @@ async fn create_dummy_users<D: DatabaseInterface>(db: &mut Database<D>) {
|
|||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
for domain in &["a", "b", "c"] {
|
||||
for letter in &["a", "b", "c"] {
|
||||
let domain = format!("{letter}.localhost");
|
||||
db.create_domain(&domain).await.unwrap().unwrap();
|
||||
db.create_user(User {
|
||||
username: domain.to_string(),
|
||||
domain: Some(format!("{domain}.localhost")),
|
||||
username: letter.to_string(),
|
||||
domain: Some(domain.clone()),
|
||||
password: "adminadmin".to_string(),
|
||||
mail: format!("{domain}@{domain}.localhost"),
|
||||
role: Role::DomainAdmin(format!("{domain}.localhost")),
|
||||
mail: format!("{letter}@{domain}"),
|
||||
role: Role::DomainAdmin(domain.clone()),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
db.create_user(User {
|
||||
username: format!("user{domain}"),
|
||||
domain: Some(format!("{domain}.localhost")),
|
||||
username: format!("user{letter}"),
|
||||
domain: Some(domain.clone()),
|
||||
password: "adminadmin".to_string(),
|
||||
mail: format!("user{domain}@{domain}.localhost"),
|
||||
mail: format!("user{letter}@{domain}"),
|
||||
role: Role::User,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
30
templates/domain.html
Normal file
30
templates/domain.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block main %}
|
||||
<main id="center">
|
||||
<img src="/assets/img/logo.png" id="logo">
|
||||
<p style="text-align: center;">You are logged in as {{ user.username }}</p>
|
||||
<div id="login-line">
|
||||
<a href="/logout" id="submit-login" value="Logout">Logout</a>
|
||||
</div>
|
||||
{% if can_create_user %}
|
||||
<div>
|
||||
<h2>Create user</h2>
|
||||
<form action="/user" method="POST">
|
||||
<input type="hidden" name="domain" value="{{ domain.name }}">
|
||||
<input type="text" name="username" placeholder="username">
|
||||
<input type="password" name="password" placeholder="password">
|
||||
<button type="submit" class="button is-info">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h2>Users on {{ domain.name }}</h2>
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user.mail }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -6,9 +6,26 @@
|
|||
<div id="login-line">
|
||||
<a href="/logout" id="submit-login" value="Logout">Logout</a>
|
||||
</div>
|
||||
{% if can_create_domain %}
|
||||
<div>
|
||||
<h2>Create domain</h2>
|
||||
<form action="/domain" method="POST">
|
||||
<input type="text" name="domainname" placeholder="example.com">
|
||||
<button type="submit" class="button is-info">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h2>Active domains you can see</h2>
|
||||
<ul>
|
||||
{% for domain in domains %}
|
||||
<li><a href="/domain/{{ domain.name }}">{{ domain.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if other_users %}
|
||||
<div>
|
||||
<h2>Other users you have permission to see</h2>
|
||||
<h2>Other users you have permission to see on your own domain</h2>
|
||||
<ul>
|
||||
{% for user in other_users %}
|
||||
<li>{{ user.mail }}</li>
|
||||
|
|
|
|||
Loading…
Reference in a new issue