refactor: Store domains separately from users

This commit is contained in:
selfhoster selfhoster 2026-09-19 09:25:25 +02:00
commit 58a8c375f0
5 changed files with 59 additions and 88 deletions

View file

@ -1,35 +1,4 @@
use crate::db::error::UserCreationError;
use crate::db::{Group, User, UserRef};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Domain { pub struct Domain {
pub name: String, pub name: String,
pub users: Vec<User>,
#[expect(unused)]
pub groups: Vec<Group>,
}
impl Domain {
pub fn get_user(&self, req_user: &UserRef) -> Option<User> {
assert!(
req_user.domain.as_deref().unwrap_or("") == self.name,
"Should only call Domain::get_user on the matching domain. Asked for {:?} on domain {}",
req_user,
self.name
);
self.users
.iter()
.find(|u| u.username == req_user.username)
.cloned()
}
pub fn create_user(&mut self, user: User, user_ref: UserRef) -> Result<(), UserCreationError> {
if self.get_user(&user_ref).is_some() {
return Err(UserCreationError::UserAlreadyExists(user_ref));
}
self.users.push(user);
Ok(())
}
} }

View file

@ -36,8 +36,12 @@ impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
.await .await
} }
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> { async fn list_all_users(&self) -> Result<Vec<User>, BoxedError> {
self.inner.read().await.list_users(domain).await 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
} }
} }
@ -63,10 +67,13 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
current_user: &User, current_user: &User,
) -> Result<Result<(), UserCreationError>, BoxedError>; ) -> Result<Result<(), UserCreationError>, BoxedError>;
/// Use empty string domain to request global users (TODO: this is not very DX) #[expect(unused)]
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>>;
/// List users on a specific domain.
/// ///
/// When domain does not exist, the returned list is empty. /// A `None` domain requested lists global service users.
fn list_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;

View file

@ -1,23 +1,21 @@
use std::future::{Future, ready}; use std::future::{Future, ready};
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
use crate::db::{Database, DatabaseInterface, Domain, Operation, User, UserRef}; use crate::db::{Database, DatabaseInterface, Domain, 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 domains: Vec<Domain>, pub domains: Vec<Domain>,
/// Where global users/groups are registered #[expect(unused)]
pub global_domain: Domain, pub groups: Vec<Group>,
pub users: Vec<User>,
} }
impl MemoryDatabase { impl MemoryDatabase {
pub fn new() -> Database<Self> { pub fn new() -> Database<Self> {
Database::new(Self::default()) Database::new(Self::default())
} }
pub fn get_domain_mut(&mut self, domain: &str) -> Option<&mut Domain> {
self.domains.iter_mut().find(|d| d.name == domain)
}
} }
impl DatabaseInterface for MemoryDatabase { impl DatabaseInterface for MemoryDatabase {
@ -39,8 +37,6 @@ impl DatabaseInterface for MemoryDatabase {
self.domains.push(Domain { self.domains.push(Domain {
name: domain.to_string(), name: domain.to_string(),
users: vec![],
groups: vec![],
}); });
ready(Ok(Ok(()))) ready(Ok(Ok(())))
@ -54,37 +50,36 @@ impl DatabaseInterface for MemoryDatabase {
ready(Ok(Some(domain.clone()))) ready(Ok(Some(domain.clone())))
} }
async fn get_user(&self, req_user: &UserRef) -> Result<Option<User>, BoxedError> { fn get_user(
let Some(req_domain) = &req_user.domain else { &self,
// If no domain is provided for the user query, look up the global users req_user: &UserRef,
return Ok(self.global_domain.get_user(req_user)); ) -> impl Future<Output = Result<Option<User>, BoxedError>> {
}; ready(Ok(self
.users
let Some(domain) = self.get_domain(req_domain).await? else { .iter()
return Ok(None); .find(|u| u.username == req_user.username && u.domain == req_user.domain)
}; .cloned()))
Ok(domain.get_user(req_user))
} }
fn create_user( async fn create_user(
&mut self, &mut self,
user: User, user: User,
) -> impl Future<Output = Result<Result<(), UserCreationError>, BoxedError>> { ) -> Result<Result<(), UserCreationError>, BoxedError> {
let user_ref = user.user_ref(); let user_ref = user.user_ref();
let Some(req_domain) = &user.domain else { if self.get_user(&user_ref).await?.is_some() {
// No domain requested, this is a global user creation return Ok(Err(UserCreationError::UserAlreadyExists(user_ref)));
return ready(Ok(self.global_domain.create_user(user, user_ref))); }
};
let Some(domain) = self.get_domain_mut(req_domain) else { // If a domain is requested (i.e. not a global user), make sure the domain exists
return ready(Ok(Err(UserCreationError::DomainNotFound( if let Some(req_domain) = &user.domain
req_domain.clone(), && self.get_domain(req_domain).await?.is_none()
)))); {
}; return Ok(Err(UserCreationError::DomainNotFound(req_domain.clone())));
}
ready(Ok(domain.create_user(user, user_ref))) self.users.push(user);
Ok(Ok(()))
} }
async fn try_create_user( async fn try_create_user(
@ -109,23 +104,19 @@ impl DatabaseInterface for MemoryDatabase {
self.create_user(new_user).await self.create_user(new_user).await
} }
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> { fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
if let Some(domain) = domain { ready(Ok(self.users.clone()))
if domain.is_empty() { }
Ok(self.global_domain.users.clone())
} else { fn list_domain_users(
let Some(domain) = self.get_domain(&domain).await? else { &self,
return Ok(vec![]); domain: Option<String>,
}; ) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
Ok(domain.users.clone()) ready(Ok(self
} .users
} else { .iter()
// Aggregate all users .filter(|u| u.domain == domain)
let mut users = self.global_domain.users.clone(); .cloned()
for domain in &self.domains { .collect()))
users.extend(domain.users.clone());
}
Ok(users)
}
} }
} }

View file

@ -115,7 +115,11 @@ pub async fn home<D: DatabaseInterface>(
// When the user has no domain (service admin) list all domains // When the user has no domain (service admin) list all domains
let op = Operation::ListUsers(session.user.domain.clone()); let op = Operation::ListUsers(session.user.domain.clone());
let other_users = if session.user.can_perform(&op) { let other_users = if session.user.can_perform(&op) {
match state.db.list_users(session.user.domain.clone()).await { match state
.db
.list_domain_users(session.user.domain.clone())
.await
{
Ok(other_users) => other_users, Ok(other_users) => other_users,
Err(e) => { Err(e) => {
return format!("Database error: {e}").into_response(); return format!("Database error: {e}").into_response();

View file

@ -8,7 +8,7 @@
</div> </div>
{% if other_users %} {% if other_users %}
<div> <div>
<h2>Other users you have permission to see</h2> <h2>Other users you have permission to see on your own domain</h2>
<ul> <ul>
{% for user in other_users %} {% for user in other_users %}
<li>{{ user.mail }}</li> <li>{{ user.mail }}</li>