fix: Properly load/save DB and stop creating dummy users

This commit is contained in:
selfhoster selfhoster 2026-09-20 13:47:46 +02:00
commit 1e0b9ac166
4 changed files with 37 additions and 43 deletions

View file

@ -1,5 +1,4 @@
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use std::fmt; use std::fmt;
@ -35,7 +34,7 @@ impl fmt::Display for FilesystemDatabaseError {
impl std::error::Error for FilesystemDatabaseError {} impl std::error::Error for FilesystemDatabaseError {}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default)]
pub struct FilesystemDatabase { pub struct FilesystemDatabase {
// TODO: Once we have common validation steps in place across DB backends, we can reduce cloning. // TODO: Once we have common validation steps in place across DB backends, we can reduce cloning.
// For now, we clone the DB on every write operation, and update it when saving to disk // For now, we clone the DB on every write operation, and update it when saving to disk
@ -59,10 +58,14 @@ impl FilesystemDatabase {
path: path.to_path_buf(), path: path.to_path_buf(),
}; };
db.save().await?; tracing::info!("Initializing empty database in file {path}. Checking permissions...");
db.save_self().await?;
tracing::info!("Database successfully created");
return Ok(Database::new(db)); return Ok(Database::new(db));
} }
tracing::info!("Loading database from file {path}");
let s = tokio::fs::read(path) let s = tokio::fs::read(path)
.await .await
.map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?; .map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?;
@ -74,13 +77,29 @@ impl FilesystemDatabase {
})) }))
} }
pub async fn save(&mut self) -> Result<(), BoxedError> { /// Attempts to save a new state of the DB, effectively switching to the new state
let s = serde_json::to_string(&self) /// only if the save is successful.
pub async fn save(&mut self, new_db: MemoryDatabase) -> Result<(), BoxedError> {
self.save_inner(&new_db).await?;
self.inner = new_db;
Ok(())
}
/// Saves an in-memory DB to disk. Cannot be used directly to observe exclusive access.
async fn save_inner(&self, inner: &MemoryDatabase) -> Result<(), BoxedError> {
let s = serde_json::to_string(inner)
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?; .map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?;
Ok(tokio::fs::write(&self.path, &s) Ok(tokio::fs::write(&self.path, &s)
.await .await
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?) .map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?)
} }
/// Save the database without switching to a new state.
///
/// Used when initializing the DB, to check for permissions.
pub async fn save_self(&mut self) -> Result<(), BoxedError> {
self.save_inner(&self.inner).await
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -95,9 +114,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e)); return Ok(Err(e));
} }
self.save().await?; self.save(new_db).await?;
self.inner = new_db;
Ok(Ok(())) Ok(Ok(()))
} }
@ -119,9 +136,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e)); return Ok(Err(e));
} }
self.save().await?; self.save(new_db).await?;
self.inner = new_db;
Ok(Ok(())) Ok(Ok(()))
} }
@ -136,9 +151,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e)); return Ok(Err(e));
} }
self.save().await?; self.save(new_db).await?;
self.inner = new_db;
Ok(Ok(())) Ok(Ok(()))
} }

View file

@ -70,7 +70,6 @@ pub trait DatabaseInterface: std::fmt::Debug + Send + Sync + 'static {
) -> Result<Result<(), UserCreationError>, BoxedError>; ) -> Result<Result<(), UserCreationError>, BoxedError>;
async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError>; async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError>;
#[expect(unused)]
async fn list_all_users(&self) -> Result<Vec<User>, BoxedError>; async fn list_all_users(&self) -> Result<Vec<User>, BoxedError>;
/// List users on a specific domain. /// List users on a specific domain.

View file

@ -14,6 +14,7 @@ pub struct MemoryDatabase {
impl MemoryDatabase { impl MemoryDatabase {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub fn new() -> Database { pub fn new() -> Database {
tracing::warn!("Using in-memory database. Data will not be saved across restarts!");
Database::new(Self::default()) Database::new(Self::default())
} }
} }

View file

@ -18,36 +18,16 @@ use ldap::ldap_handler;
use listener::ListenerPath; use listener::ListenerPath;
async fn create_dummy_users(db: &mut Database) { async fn create_dummy_users(db: &mut Database) {
db.create_user(User { // TODO: customize admin password
username: "admin".to_string(), if db.list_all_users().await.unwrap().is_empty() {
domain: None, tracing::info!("Creating admin account with default `adminadmin` password");
password: "adminadmin".to_string(),
// TODO: what should we put here?
mail: "admin".to_string(),
role: Role::Admin,
})
.await
.unwrap()
.unwrap();
for letter 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: "admin".to_string(),
domain: Some(domain.clone()), domain: None,
password: "adminadmin".to_string(), password: "adminadmin".to_string(),
mail: format!("{letter}@{domain}"), // TODO: what should we put here?
role: Role::DomainAdmin(domain.clone()), mail: "admin".to_string(),
}) role: Role::Admin,
.await
.unwrap()
.unwrap();
db.create_user(User {
username: format!("user{letter}"),
domain: Some(domain.clone()),
password: "adminadmin".to_string(),
mail: format!("user{letter}@{domain}"),
role: Role::User,
}) })
.await .await
.unwrap() .unwrap()
@ -74,6 +54,7 @@ async fn main() -> Result<(), GlobalError> {
} else { } else {
MemoryDatabase::new() MemoryDatabase::new()
}; };
tracing::info!("Database loaded successfully");
create_dummy_users(&mut db).await; create_dummy_users(&mut db).await;
#[cfg(feature = "http")] #[cfg(feature = "http")]