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 serde::{Deserialize, Serialize};
use serde_json::Error as JsonError;
use std::fmt;
@ -35,7 +34,7 @@ impl fmt::Display for FilesystemDatabaseError {
impl std::error::Error for FilesystemDatabaseError {}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default)]
pub struct FilesystemDatabase {
// 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
@ -59,10 +58,14 @@ impl FilesystemDatabase {
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));
}
tracing::info!("Loading database from file {path}");
let s = tokio::fs::read(path)
.await
.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> {
let s = serde_json::to_string(&self)
/// Attempts to save a new state of the DB, effectively switching to the new state
/// 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)))?;
Ok(tokio::fs::write(&self.path, &s)
.await
.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]
@ -95,9 +114,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e));
}
self.save().await?;
self.inner = new_db;
self.save(new_db).await?;
Ok(Ok(()))
}
@ -119,9 +136,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e));
}
self.save().await?;
self.inner = new_db;
self.save(new_db).await?;
Ok(Ok(()))
}
@ -136,9 +151,7 @@ impl DatabaseInterface for FilesystemDatabase {
return Ok(Err(e));
}
self.save().await?;
self.inner = new_db;
self.save(new_db).await?;
Ok(Ok(()))
}

View file

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

View file

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

View file

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