@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user