#![recursion_limit = "256"] //! Owner app server entry point. //! //! This server runs on port 3001 and serves the admin UI with the //! `chattyness_owner` database role (no RLS restrictions). #[cfg(feature = "ssr")] mod server { use axum::{Router, response::Redirect, routing::get}; use clap::Parser; use leptos::prelude::*; use leptos_axum::{LeptosRoutes, generate_route_list}; use sqlx::postgres::PgPoolOptions; use std::net::SocketAddr; use std::path::Path; use tower_http::services::ServeDir; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use chattyness_admin_ui::{AdminApp, AdminAppState, admin_shell}; /// CLI arguments. #[derive(Parser)] #[command(name = "chattyness-owner")] #[command(about = "Chattyness Owner Admin Server")] struct Args { /// Host to bind to #[arg(long, env = "HOST", default_value = "127.0.0.1")] host: String, /// Port to bind to #[arg(long, env = "OWNER_PORT", default_value = "3001")] port: u16, /// Database password for chattyness_owner role #[arg(long, env = "DB_CHATTYNESS_OWNER")] db_password: String, /// Use secure cookies #[arg(long, env = "SECURE_COOKIES", default_value = "false")] secure_cookies: bool, } pub async fn main() -> Result<(), Box> { // Load environment variables dotenvy::dotenv().ok(); // Initialize logging tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "chattyness_owner=debug,tower_http=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // Parse arguments let args = Args::parse(); tracing::info!("Starting Chattyness Owner Server"); // Create database pool for owner access (fixed connection string) let database_url = format!( "postgres://chattyness_owner:{}@localhost/chattyness", args.db_password ); let pool = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await?; tracing::info!("Connected to database (owner role)"); // Configure Leptos let cargo_toml = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"); let conf = get_configuration(Some(cargo_toml)).unwrap(); let leptos_options = conf.leptos_options; let addr = SocketAddr::new(args.host.parse()?, args.port); // Create session layer let session_layer = chattyness_admin_ui::auth::create_admin_session_layer( pool.clone(), args.secure_cookies, ) .await; // Create app state let app_state = AdminAppState { pool: pool.clone(), leptos_options: leptos_options.clone(), }; // Generate routes let routes = generate_route_list(AdminApp); // Get site paths from Leptos config // site_root is relative to workspace root, make it absolute let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir.parent().unwrap().parent().unwrap(); let site_root = workspace_root.join(&*leptos_options.site_root); let static_dir = site_root.join("static"); let favicon_path = manifest_dir.join("public/favicon.ico"); tracing::info!("Serving static files from: {}", site_root.display()); // Admin CSS path let admin_css_path = static_dir.join("chattyness-owner.css"); // Shared assets directory for uploaded files (realm images, etc.) let assets_dir = Path::new("/srv/chattyness/assets"); // Create admin connection layer for RLS context let admin_conn_layer = chattyness_admin_ui::auth::AdminConnLayer::new(pool.clone()); // Build the app let app = Router::new() // Redirect root to admin .route("/", get(|| async { Redirect::permanent("/admin") })) // Nest API routes under /api/admin (matches frontend expectations when UI is at /admin) .nest( "/api/admin", chattyness_admin_ui::api::admin_api_router() .layer(admin_conn_layer) .with_state(app_state.clone()), ) // Uploaded assets (realm backgrounds, props, etc.) - must come before /static .nest_service("/assets/server", ServeDir::new(assets_dir.join("server"))) .nest_service("/static/realm", ServeDir::new(assets_dir.join("realm"))) // Static files (build output: JS, CSS, WASM) .nest_service("/static", ServeDir::new(&static_dir)) .nest_service( "/favicon.ico", tower_http::services::ServeFile::new(&favicon_path), ) // Serve admin CSS at standardized path .nest_service( "/static/css/admin.css", tower_http::services::ServeFile::new(&admin_css_path), ) // Leptos routes .leptos_routes(&app_state, routes, { let leptos_options = leptos_options.clone(); move || admin_shell(leptos_options.clone()) }) // Apply session middleware .layer(session_layer) .with_state(app_state); tracing::info!("Listening on http://{}", addr); // Start server let listener = tokio::net::TcpListener::bind(&addr).await?; axum::serve(listener, app.into_make_service()).await?; Ok(()) } } #[cfg(feature = "ssr")] #[tokio::main] async fn main() -> Result<(), Box> { server::main().await } #[cfg(not(feature = "ssr"))] fn main() { // This is for WASM build, which is handled by lib.rs }