update to support user expire, timeout, and disconnect

This commit is contained in:
Evan Carroll 2026-01-17 23:47:02 -06:00
parent fe65835f4a
commit 5fcd49e847
16 changed files with 744 additions and 238 deletions

View file

@ -15,6 +15,7 @@ chattyness-admin-ui.workspace = true
chattyness-user-ui.workspace = true
chattyness-db.workspace = true
chattyness-error.workspace = true
chattyness-shared.workspace = true
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
@ -57,6 +58,7 @@ ssr = [
"chattyness-user-ui/ssr",
"chattyness-db/ssr",
"chattyness-error/ssr",
"chattyness-shared/ssr",
]
# Unified hydrate feature - admin routes are lazy-loaded via #[lazy] macro
hydrate = [

View file

@ -14,12 +14,14 @@ mod server {
use leptos_axum::{generate_route_list, LeptosRoutes};
use sqlx::postgres::PgPoolOptions;
use std::net::SocketAddr;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tower_http::services::ServeDir;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use chattyness_app::{combined_shell, CombinedApp, CombinedAppState};
use chattyness_shared::AppConfig;
use chattyness_user_ui::api::WebSocketState;
/// CLI arguments.
@ -42,6 +44,10 @@ mod server {
/// Use secure cookies
#[arg(long, env = "SECURE_COOKIES", default_value = "false")]
secure_cookies: bool,
/// Path to TOML configuration file
#[arg(long, short = 'c', env = "CONFIG_FILE")]
config: Option<PathBuf>,
}
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -60,6 +66,10 @@ mod server {
// Parse arguments
let args = Args::parse();
// Load configuration
let config = AppConfig::load(args.config.as_deref())?;
tracing::info!("Configuration loaded: {:?}", config);
tracing::info!("Starting Chattyness App Server");
// Create database pool for app access (fixed connection string, RLS-constrained)
@ -74,6 +84,53 @@ mod server {
tracing::info!("Connected to database (app role with RLS)");
// Startup cleanup: clear all instance_members if configured
// Uses SECURITY DEFINER function to bypass RLS
if config.cleanup.clear_on_startup {
tracing::info!("Clearing stale instance_members on startup...");
let deleted: i64 = sqlx::query_scalar("SELECT scene.clear_all_instance_members()")
.fetch_one(&pool)
.await?;
tracing::info!("Cleared {} stale instance_members", deleted);
}
// Spawn background task for periodic stale member cleanup
// Uses SECURITY DEFINER function to bypass RLS
{
let cleanup_pool = pool.clone();
let cleanup_config = config.cleanup.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(
cleanup_config.reap_interval_secs,
));
loop {
interval.tick().await;
let threshold = cleanup_config.stale_threshold_secs as f64;
match sqlx::query_scalar::<_, i64>(
"SELECT scene.clear_stale_instance_members($1)",
)
.bind(threshold)
.fetch_one(&cleanup_pool)
.await
{
Ok(deleted) => {
if deleted > 0 {
tracing::info!("Reaped {} stale instance_members", deleted);
}
}
Err(e) => {
tracing::error!("Stale member cleanup failed: {:?}", e);
}
}
}
});
tracing::info!(
"Started stale member reaper (interval: {}s, threshold: {}s)",
config.cleanup.reap_interval_secs,
config.cleanup.stale_threshold_secs
);
}
// Configure Leptos
let cargo_toml = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
let conf = get_configuration(Some(cargo_toml)).unwrap();
@ -118,6 +175,7 @@ mod server {
pool: pool.clone(),
leptos_options: leptos_options.clone(),
ws_state: ws_state.clone(),
ws_config: config.websocket.clone(),
};
let admin_api_state = chattyness_admin_ui::AdminAppState {
pool: pool.clone(),