Compare commits
17 changed files with 91 additions and 444 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -52,7 +52,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -120,17 +119,6 @@ dependencies = [
|
||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argh = "0.1.19"
|
argh = "0.1.19"
|
||||||
axum = { version = "0.8.9", optional = true, features = ["macros"] }
|
axum = { version = "0.8.9", optional = true }
|
||||||
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
||||||
camino = "1.2.5"
|
camino = "1.2.5"
|
||||||
dn_escape = { path = "vendor/dn_escape" }
|
dn_escape = { path = "vendor/dn_escape" }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use tokio::sync::RwLock;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::db::error::BoxedError;
|
use crate::db::error::BoxedError;
|
||||||
use crate::db::{DatabaseInterface, Domain, User, UserRef};
|
use crate::db::{DatabaseInterface, UserRef};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Database<D: DatabaseInterface> {
|
pub struct Database<D: DatabaseInterface> {
|
||||||
|
|
@ -36,13 +36,4 @@ impl<D: DatabaseInterface> Database<D> {
|
||||||
tracing::debug!("Comparing {} and {}", user.password, password);
|
tracing::debug!("Comparing {} and {}", user.password, password);
|
||||||
Ok(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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
|
||||||
pub struct Domain {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ pub type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UserCreationError {
|
pub enum UserCreationError {
|
||||||
DomainNotFound(String),
|
|
||||||
UserAlreadyExists(UserRef),
|
UserAlreadyExists(UserRef),
|
||||||
Permissions,
|
Permissions,
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +13,6 @@ pub enum UserCreationError {
|
||||||
impl fmt::Display for UserCreationError {
|
impl fmt::Display for UserCreationError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::DomainNotFound(domain) => write!(f, "No domain {domain} to create user in"),
|
|
||||||
Self::UserAlreadyExists(user) => write!(f, "User already exists: {user}"),
|
Self::UserAlreadyExists(user) => write!(f, "User already exists: {user}"),
|
||||||
Self::Permissions => write!(f, "You do not have permissions to create this user"),
|
Self::Permissions => write!(f, "You do not have permissions to create this user"),
|
||||||
}
|
}
|
||||||
|
|
@ -22,25 +20,3 @@ impl fmt::Display for UserCreationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error 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,18 +1,7 @@
|
||||||
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
use crate::db::error::{BoxedError, UserCreationError};
|
||||||
use crate::db::{Database, Domain, User, UserRef};
|
use crate::db::{Database, User, UserRef};
|
||||||
|
|
||||||
impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
|
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> {
|
async fn get_user(&self, user: &UserRef) -> Result<Option<User>, BoxedError> {
|
||||||
self.inner.read().await.get_user(user).await
|
self.inner.read().await.get_user(user).await
|
||||||
}
|
}
|
||||||
|
|
@ -36,29 +25,12 @@ impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError> {
|
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> {
|
||||||
self.inner.read().await.list_all_domains().await
|
self.inner.read().await.list_users(domain).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 {
|
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(
|
fn get_user(
|
||||||
&self,
|
&self,
|
||||||
user: &UserRef,
|
user: &UserRef,
|
||||||
|
|
@ -67,20 +39,13 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
|
||||||
&mut self,
|
&mut self,
|
||||||
user: User,
|
user: User,
|
||||||
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
||||||
fn try_create_user(
|
#[expect(unused)]
|
||||||
|
async fn try_create_user(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_user: User,
|
new_user: User,
|
||||||
current_user: &User,
|
current_user: &User,
|
||||||
) -> impl Future<Output = Result<Result<(), UserCreationError>, BoxedError>> + Send;
|
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
||||||
|
fn list_users(
|
||||||
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,
|
&self,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
) -> impl std::future::Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
) -> impl std::future::Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
use std::future::{Future, ready};
|
use std::future::{Future, ready};
|
||||||
|
|
||||||
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
use crate::db::error::{BoxedError, UserCreationError};
|
||||||
use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef};
|
use crate::db::{Database, DatabaseInterface, Group, Operation, User, UserRef};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct MemoryDatabase {
|
pub struct MemoryDatabase {
|
||||||
// We store data in tables like in SQL
|
pub users: Vec<User>,
|
||||||
pub domains: Vec<Domain>,
|
|
||||||
#[expect(unused)]
|
#[expect(unused)]
|
||||||
pub groups: Vec<Group>,
|
pub groups: Vec<Group>,
|
||||||
pub users: Vec<User>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryDatabase {
|
impl MemoryDatabase {
|
||||||
|
|
@ -19,46 +17,17 @@ impl MemoryDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DatabaseInterface for 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(
|
fn get_user(
|
||||||
&self,
|
&self,
|
||||||
req_user: &UserRef,
|
req_user: &UserRef,
|
||||||
) -> impl Future<Output = Result<Option<User>, BoxedError>> {
|
) -> impl Future<Output = Result<Option<User>, BoxedError>> {
|
||||||
ready(Ok(self
|
for user in &self.users {
|
||||||
.users
|
if user.username == req_user.username && user.domain == req_user.domain {
|
||||||
.iter()
|
return ready(Ok(Some(user.clone())));
|
||||||
.find(|u| u.username == req_user.username && u.domain == req_user.domain)
|
}
|
||||||
.cloned()))
|
}
|
||||||
|
|
||||||
|
ready(Ok(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
|
|
@ -66,18 +35,10 @@ impl DatabaseInterface for MemoryDatabase {
|
||||||
user: User,
|
user: User,
|
||||||
) -> Result<Result<(), UserCreationError>, BoxedError> {
|
) -> Result<Result<(), UserCreationError>, BoxedError> {
|
||||||
let user_ref = user.user_ref();
|
let user_ref = user.user_ref();
|
||||||
|
|
||||||
if self.get_user(&user_ref).await?.is_some() {
|
if self.get_user(&user_ref).await?.is_some() {
|
||||||
return Ok(Err(UserCreationError::UserAlreadyExists(user_ref)));
|
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);
|
self.users.push(user);
|
||||||
Ok(Ok(()))
|
Ok(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +52,7 @@ impl DatabaseInterface for MemoryDatabase {
|
||||||
//
|
//
|
||||||
// TODO: for now we don't allow creating service users manually
|
// TODO: for now we don't allow creating service users manually
|
||||||
// so we assume there's a domain provided
|
// 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 {
|
let Some(new_user_domain) = &new_user.domain else {
|
||||||
return Ok(Err(UserCreationError::Permissions));
|
return Ok(Err(UserCreationError::Permissions));
|
||||||
};
|
};
|
||||||
|
|
@ -103,23 +65,19 @@ impl DatabaseInterface for MemoryDatabase {
|
||||||
self.create_user(new_user).await
|
self.create_user(new_user).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_all_domains(&self) -> impl Future<Output = Result<Vec<Domain>, BoxedError>> {
|
fn list_users(
|
||||||
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,
|
&self,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
||||||
ready(Ok(self
|
let users = if let Some(domain) = domain {
|
||||||
.users
|
self.users
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|u| u.domain == domain)
|
.filter(|user| user.domain.as_ref() == Some(&domain))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()))
|
.collect()
|
||||||
|
} else {
|
||||||
|
self.users.clone()
|
||||||
|
};
|
||||||
|
ready(Ok(users))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
mod common;
|
mod common;
|
||||||
pub use common::Database;
|
pub use common::Database;
|
||||||
mod domain;
|
|
||||||
pub use domain::Domain;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod group;
|
mod group;
|
||||||
pub use group::Group;
|
pub use group::Group;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
CreateDomain,
|
|
||||||
CreateUser(String),
|
CreateUser(String),
|
||||||
ListUsers(Option<String>),
|
ListUsers(Option<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
/// Can do anything
|
/// Can do anything
|
||||||
Admin,
|
Admin,
|
||||||
|
|
@ -29,7 +28,6 @@ pub enum Role {
|
||||||
impl Role {
|
impl Role {
|
||||||
pub fn can_perform(&self, operation: &Operation) -> bool {
|
pub fn can_perform(&self, operation: &Operation) -> bool {
|
||||||
match operation {
|
match operation {
|
||||||
Operation::CreateDomain => self == &Self::Admin,
|
|
||||||
Operation::CreateUser(op_domain) => match self {
|
Operation::CreateUser(op_domain) => match self {
|
||||||
Self::Admin => true,
|
Self::Admin => true,
|
||||||
Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => {
|
Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => {
|
||||||
|
|
@ -46,12 +44,4 @@ 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,16 +80,6 @@ impl User {
|
||||||
pub fn can_perform(&self, operation: &Operation) -> bool {
|
pub fn can_perform(&self, operation: &Operation) -> bool {
|
||||||
self.role.can_perform(operation)
|
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 {
|
impl fmt::Display for User {
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
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,25 +1,26 @@
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::response::{Html, IntoResponse, Response};
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::serve::Listener as AxumListener;
|
use axum::serve::Listener as AxumListener;
|
||||||
use minijinja::Environment;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
use http::StatusCode;
|
||||||
#[cfg(not(feature = "embed"))]
|
#[cfg(not(feature = "embed"))]
|
||||||
use minijinja::path_loader;
|
use minijinja::path_loader;
|
||||||
|
use minijinja::{Environment, context};
|
||||||
#[cfg(feature = "embed")]
|
#[cfg(feature = "embed")]
|
||||||
use static_serve::embed_assets;
|
use static_serve::embed_assets;
|
||||||
#[cfg(not(feature = "embed"))]
|
#[cfg(not(feature = "embed"))]
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::db::{Database, DatabaseInterface};
|
use crate::db::{Database, DatabaseInterface, Operation};
|
||||||
use crate::listener::{Listener, ListenerKind};
|
use crate::listener::{Listener, ListenerKind};
|
||||||
use crate::stream::{AbstractSocketAddr, AbstractStreamKind};
|
use crate::stream::{AbstractSocketAddr, AbstractStreamKind};
|
||||||
|
|
||||||
mod domain;
|
|
||||||
mod home;
|
|
||||||
mod login;
|
mod login;
|
||||||
mod logout;
|
mod logout;
|
||||||
mod session;
|
mod session;
|
||||||
use session::HttpSessionManager;
|
use session::HttpSessionManager;
|
||||||
mod user;
|
|
||||||
|
|
||||||
impl AxumListener for Listener {
|
impl AxumListener for Listener {
|
||||||
type Io = AbstractStreamKind;
|
type Io = AbstractStreamKind;
|
||||||
|
|
@ -97,14 +98,52 @@ pub async fn http_listen<D: DatabaseInterface>(listener: Listener, db: Database<
|
||||||
#[cfg(not(feature = "embed"))]
|
#[cfg(not(feature = "embed"))]
|
||||||
let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) };
|
let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) };
|
||||||
let app = app
|
let app = app
|
||||||
.route("/", get(home::home))
|
.route("/", get(home))
|
||||||
.route("/login", get(home::home))
|
.route("/login", get(home))
|
||||||
.route("/login", post(login::post_login))
|
.route("/login", post(login::post_login))
|
||||||
.route("/logout", get(logout::logout))
|
.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));
|
.with_state(HttpState::new(db));
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
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,24 +29,22 @@ async fn create_dummy_users<D: DatabaseInterface>(db: &mut Database<D>) {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for letter in &["a", "b", "c"] {
|
for domain in &["a", "b", "c"] {
|
||||||
let domain = format!("{letter}.localhost");
|
|
||||||
db.create_domain(&domain).await.unwrap().unwrap();
|
|
||||||
db.create_user(User {
|
db.create_user(User {
|
||||||
username: letter.to_string(),
|
username: domain.to_string(),
|
||||||
domain: Some(domain.clone()),
|
domain: Some(format!("{domain}.localhost")),
|
||||||
password: "adminadmin".to_string(),
|
password: "adminadmin".to_string(),
|
||||||
mail: format!("{letter}@{domain}"),
|
mail: format!("{domain}@{domain}.localhost"),
|
||||||
role: Role::DomainAdmin(domain.clone()),
|
role: Role::DomainAdmin(format!("{domain}.localhost")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.create_user(User {
|
db.create_user(User {
|
||||||
username: format!("user{letter}"),
|
username: format!("user{domain}"),
|
||||||
domain: Some(domain.clone()),
|
domain: Some(format!("{domain}.localhost")),
|
||||||
password: "adminadmin".to_string(),
|
password: "adminadmin".to_string(),
|
||||||
mail: format!("user{letter}@{domain}"),
|
mail: format!("user{domain}@{domain}.localhost"),
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{% 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,26 +6,9 @@
|
||||||
<div id="login-line">
|
<div id="login-line">
|
||||||
<a href="/logout" id="submit-login" value="Logout">Logout</a>
|
<a href="/logout" id="submit-login" value="Logout">Logout</a>
|
||||||
</div>
|
</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 %}
|
{% if other_users %}
|
||||||
<div>
|
<div>
|
||||||
<h2>Other users you have permission to see on your own domain</h2>
|
<h2>Other users you have permission to see</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for user in other_users %}
|
{% for user in other_users %}
|
||||||
<li>{{ user.mail }}</li>
|
<li>{{ user.mail }}</li>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue