changes
All checks were successful
CI / ci (push) Successful in 19m38s

This commit is contained in:
2026-08-26 20:55:30 +02:00
parent a557c183e9
commit 23d052278a
523 changed files with 24448 additions and 2005 deletions

View File

@@ -8,6 +8,7 @@ name = "k-mood"
path = "src/main.rs"
[dependencies]
bootstrap.workspace = true
domain.workspace = true
axum.workspace = true
application.workspace = true
@@ -18,6 +19,9 @@ auth.workspace = true
storage.workspace = true
event-publisher.workspace = true
importer.workspace = true
crypto.workspace = true
music.workspace = true
reqwest.workspace = true
exporter.workspace = true
web-push-adapter.workspace = true
serde.workspace = true

View File

@@ -1,17 +0,0 @@
use config::AppConfig;
const CONFIG_FILE: &str = "config.toml";
pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
let path = std::path::Path::new(CONFIG_FILE);
if path.exists() {
let content = std::fs::read_to_string(path)?;
let config: AppConfig = toml::from_str(&content)?;
tracing::info!(path = CONFIG_FILE, "loaded config from file");
Ok(config)
} else {
tracing::info!("no config file found, using defaults");
Ok(AppConfig::default())
}
}

View File

@@ -1,119 +0,0 @@
use std::sync::Arc;
use config::AppConfig;
use domain::ports::ReminderSenderPort;
use http_axum::state::AppState;
pub struct AppContext {
pub state: AppState,
pub reminder_sender: Option<Arc<dyn ReminderSenderPort>>,
}
pub async fn build(config: AppConfig) -> Result<AppContext, Box<dyn std::error::Error>> {
let db_url = config
.storage
.database_url
.clone()
.unwrap_or_else(|| format!("sqlite://{}/k-mood.db", config.storage.data_dir));
let pool = sqlite::create_pool(&db_url).await?;
sqlite::run_migrations(&pool).await?;
let user_command = Arc::new(sqlite::repositories::SqliteUserCommandRepository::new(
pool.clone(),
));
let user_query = Arc::new(sqlite::repositories::SqliteUserQueryRepository::new(
pool.clone(),
));
let activity_command = Arc::new(sqlite::repositories::SqliteActivityCommandRepository::new(
pool.clone(),
));
let activity_query = Arc::new(sqlite::repositories::SqliteActivityQueryRepository::new(
pool.clone(),
));
let entry_command = Arc::new(sqlite::repositories::SqliteEntryCommandRepository::new(
pool.clone(),
));
let entry_query = Arc::new(sqlite::repositories::SqliteEntryQueryRepository::new(
pool.clone(),
));
let reminder_command = Arc::new(sqlite::repositories::SqliteReminderCommandRepository::new(
pool.clone(),
));
let reminder_query = Arc::new(sqlite::repositories::SqliteReminderQueryRepository::new(
pool.clone(),
));
let refresh_session_command =
Arc::new(sqlite::repositories::SqliteRefreshSessionCommandRepository::new(pool.clone()));
let refresh_session_query =
Arc::new(sqlite::repositories::SqliteRefreshSessionQueryRepository::new(pool.clone()));
let cascade = Arc::new(sqlite::repositories::SqliteCascadeDeleteRepository::new(
pool.clone(),
));
let push_subscription_command =
Arc::new(sqlite::repositories::SqlitePushSubscriptionCommandRepository::new(pool.clone()));
let push_subscription_query =
Arc::new(sqlite::repositories::SqlitePushSubscriptionQueryRepository::new(pool));
let jwt_service = auth::JwtAuthService::new(&config.auth)?;
let password_hasher = auth::Argon2PasswordHasher;
let media_storage = storage::create_media_storage(&config.storage)?;
let reminder_sender: Option<Arc<dyn ReminderSenderPort>> = if config.push.enabled {
let sender =
web_push_adapter::WebPushSender::new(&config.push, push_subscription_query.clone())?;
Some(Arc::new(sender))
} else {
None
};
let (event_publisher, event_receiver) = event_publisher::create_event_channel();
spawn_event_drain(event_receiver);
let state = AppState {
entry_command,
entry_query,
activity_command,
activity_query,
user_command,
user_query,
reminder_command,
reminder_query,
refresh_session_command,
refresh_session_query,
cascade,
auth_service: Arc::new(jwt_service),
password_hasher: Arc::new(password_hasher),
event_publisher: Arc::new(event_publisher),
media_storage: Arc::new(media_storage),
export_port: Arc::new(exporter::JsonExportAdapter),
import_source: Arc::new(importer::DaylioImportAdapter),
push_subscription_command,
push_subscription_query,
reminder_sender: reminder_sender.clone(),
server_config: config.server,
entry_config: config.entry,
auth_config: config.auth,
push_config: config.push,
preset_config: config.preset,
};
Ok(AppContext {
state,
reminder_sender,
})
}
fn spawn_event_drain(mut receiver: event_publisher::EventReceiver) {
tokio::spawn(async move {
while let Some(envelope) = receiver.recv().await {
tracing::debug!(
event_id = %envelope.id(),
event = ?envelope.event(),
"domain event published"
);
}
tracing::info!("event drain stopped");
});
}

View File

@@ -1,18 +1,15 @@
mod config_loader;
mod factory;
#[tokio::main]
async fn main() {
setup_tracing();
bootstrap::setup_tracing();
if let Err(e) = run().await {
tracing::error!(error = %e, "application failed");
if let Err(error) = run().await {
tracing::error!(%error, "the server could not start");
std::process::exit(1);
}
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let config = config_loader::load()?;
let config = bootstrap::load()?;
tracing::info!(
host = %config.server.host,
@@ -20,112 +17,22 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
"starting k-mood server"
);
let context = factory::build(config.clone()).await?;
if let Some(sender) = context.reminder_sender {
spawn_reminder_scheduler(
context.state.reminder_query.clone(),
context.state.user_query.clone(),
sender,
);
tracing::info!("push notifications enabled, reminder scheduler started");
}
spawn_session_cleanup(context.state.refresh_session_command.clone());
let context = bootstrap::build(config.clone()).await?;
let router = http_axum::router::build_router(context.state);
let addr = format!("{}:{}", config.server.host, config.server.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
let address = format!("{}:{}", config.server.host, config.server.port);
let listener = tokio::net::TcpListener::bind(&address).await?;
tracing::info!(addr = %addr, "listening");
tracing::info!(
address = %address,
"listening; background work runs in the k-mood-worker process"
);
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.with_graceful_shutdown(bootstrap::shutdown::on_signal())
.await?;
tracing::info!("server shut down gracefully");
tracing::info!("k-mood server shut down gracefully");
Ok(())
}
fn spawn_reminder_scheduler(
reminder_query: std::sync::Arc<dyn domain::ports::ReminderQueryPort>,
user_query: std::sync::Arc<dyn domain::ports::UserQueryPort>,
sender: std::sync::Arc<dyn domain::ports::ReminderSenderPort>,
) {
use application::reminder::use_cases::process_due_reminders;
tokio::spawn(async move {
let deps = process_due_reminders::Deps {
reminder_query,
user_query,
sender,
};
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
loop {
interval.tick().await;
match process_due_reminders::execute(&deps).await {
Ok(sent) => {
if sent > 0 {
tracing::info!(sent, "reminders sent");
}
}
Err(e) => tracing::error!(error = %e, "reminder processing failed"),
}
}
});
}
fn spawn_session_cleanup(
refresh_session_command: std::sync::Arc<dyn domain::ports::RefreshSessionCommandPort>,
) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
loop {
interval.tick().await;
match refresh_session_command.delete_expired().await {
Ok(deleted) => {
if deleted > 0 {
tracing::info!(deleted, "expired refresh sessions cleaned up");
}
}
Err(e) => tracing::error!(error = %e, "refresh session cleanup failed"),
}
}
});
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("received Ctrl+C"),
_ = terminate => tracing::info!("received SIGTERM"),
}
}
fn setup_tracing() {
use tracing_subscriber::EnvFilter;
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
)
.init();
}