use std::{net::IpAddr, sync::atomic::Ordering, time::{Duration, Instant}}; use axum::{ body::Body, extract::{Path, Query, Request, State, WebSocketUpgrade, ws::{Message, WebSocket}}, http::{header, HeaderMap, HeaderValue, StatusCode}, middleware::{self, Next}, response::{Redirect, Response}, routing::{get, post}, Json, Router, }; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{Duration as ChronoDuration, NaiveTime, Utc}; use futures_util::StreamExt; use rand::{rngs::OsRng, RngCore}; use serde::Deserialize; use sha2::{Digest, Sha256}; use serde_json::{json, Value}; use tower_http::{compression::CompressionLayer, trace::TraceLayer}; use uuid::Uuid; use crate::{ engine, error::AppError, home_assistant, influxdb, notifications, models::{ApiTokenInfo, Automation, ClimateGroup, ConfigurationExport, DebugSettings, Device, DeviceCommand, DevicePatch, DiscoveryRequest, GroupControlPatch, ManualDeviceRequest, HaReading, NotificationSettings, Reading, RuntimeSettings, Schedule, TemporaryQuickThermostat, Zone, ZoneControlPatch, ZoneReading}, protocol::merge_discovered, state::AppState, }; const INDEX_HTML: &str = include_str!("../web/index.html"); const NOT_FOUND_HTML: &str = include_str!("../web/404.html"); const APP_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/app.bundle.js")); const THEME_INIT_JS: &str = include_str!("../web/theme-init.js"); const STYLES_CSS: &str = include_str!("../web/styles.css"); const MANIFEST: &str = include_str!("../web/manifest.webmanifest"); const SERVICE_WORKER: &str = include_str!("../web/sw.js"); const FAVICON: &str = include_str!("../web/favicon.svg"); include!(concat!(env!("OUT_DIR"), "/languages.rs")); const SPA_ROUTES: &[&str] = &[ "/dashboard", "/devices", "/zones", "/groups", "/schedules", "/automations", "/flows", "/simulation", "/night-mode", "/home-assistant", "/settings", "/events", "/history", "/history/overview", "/history/zones", "/history/devices", "/history/sensors", "/history/custom", ]; pub fn router(state: AppState) -> Router { let protected = Router::new() .route("/api/bootstrap", get(bootstrap)) .route("/api/system/info", get(system_info)) .route("/api/discovery", post(discover)) .route("/api/devices", get(list_devices).post(add_device)) .route("/api/devices/:id", get(get_device).patch(patch_device).delete(delete_device)) .route("/api/devices/:id/bind", post(bind_device)) .route("/api/devices/:id/poll", post(poll_device)) .route("/api/devices/:id/command", post(command_device)) .route("/api/zones", get(list_zones).post(create_zone)) .route("/api/zones/:id", get(get_zone).put(update_zone).delete(delete_zone)) .route("/api/zones/:id/control", post(update_zone_control)) .route("/api/zones/:id/compressor-queue/cancel", post(cancel_zone_compressor_queue)) .route("/api/compressor-queue/cancel-all", post(cancel_all_compressor_queues)) .route("/api/zones/:id/schedule-template", post(apply_schedule_template)) .route("/api/groups", get(list_groups).post(create_group)) .route("/api/groups/:id", get(get_group).put(update_group).delete(delete_group)) .route("/api/groups/:id/control", post(update_group_control)) .route("/api/house/control", post(update_house_control)) .route("/api/house/power", post(update_house_power)) .route("/api/house/preset", post(update_house_preset)) .route("/api/schedules", get(list_schedules).post(create_schedule)) .route("/api/schedules/:id", get(get_schedule).put(update_schedule).delete(delete_schedule)) .route("/api/automations", get(list_automations).post(create_automation)) .route("/api/automations/:id", get(get_automation).put(update_automation).delete(delete_automation)) .route("/api/flows", get(list_flows).post(create_flow)) .route("/api/flows/import", post(import_flow)) .route("/api/flows/simulate", post(simulate_flow)) .route("/api/flows/:id/export", get(export_flow)) .route("/api/flows/:id/logs", get(flow_logs)) .route("/api/flows/:id", get(get_flow).put(update_flow).delete(delete_flow)) .route("/api/readings", get(readings)) .route("/api/history", get(history)) .route("/api/control-plan", get(control_plan)) .route("/api/events", get(events)) .route("/api/events/retention", get(get_event_retention).put(update_event_retention)) .route("/api/settings", get(get_settings).put(update_settings)) .route("/api/settings/export", get(export_settings)) .route("/api/settings/import", post(import_settings)) .route("/api/debug", get(get_debug).put(update_debug)) .route("/api/access-tokens", get(list_access_tokens).post(create_access_token)) .route("/api/access-tokens/:id", axum::routing::delete(delete_access_token)) .route("/api/integrations/home-assistant/test", post(test_home_assistant)) .route("/api/integrations/notifications/test", post(test_notifications)) .route_layer(middleware::from_fn_with_state(state.clone(), auth)); let home_assistant_api = Router::new() .route("/api/integrations/home-assistant/devices", get(list_devices)) .route("/api/integrations/home-assistant/devices/:id/command", post(command_home_assistant_device)) .route("/api/integrations/home-assistant/control-plan", get(control_plan)) .route("/api/integrations/home-assistant/groups", get(list_home_assistant_groups)) .route("/api/integrations/home-assistant/groups/:id/control", post(update_home_assistant_group_control)) .route("/api/integrations/home-assistant/house/control", post(update_house_control)) .route("/api/integrations/home-assistant/house/preset", post(update_house_preset)) .route("/api/integrations/home-assistant/house/power", post(update_house_power)) .route("/api/integrations/home-assistant/zones/:id/control", post(update_home_assistant_zone_control)) .route_layer(middleware::from_fn_with_state(state.clone(), home_assistant_auth)); let mut app = Router::new() .route("/api/health", get(health)) .route("/ws", get(websocket)) .route("/", get(index)) .route("/index.html", get(index)) .route(APP_JS_ASSET_PATH, get(app_js)) .route(THEME_INIT_ASSET_PATH, get(theme_init_js)) .route(STYLES_CSS_ASSET_PATH, get(styles_css)) .route("/app.js", get(app_js_legacy)) .route("/theme-init.js", get(theme_init_js_legacy)) .route("/styles.css", get(styles_css_legacy)) .route("/manifest.webmanifest", get(manifest)) .route("/sw.js", get(service_worker)) .route("/favicon.svg", get(favicon)) .route("/flows/:id", get(index)) .route("/lang/index.json", get(language_index)) .route("/lang/:file", get(language_file)) .route("/presets/index.json", get(preset_index)) .route("/presets/:file", get(preset_file)) .merge(protected) .merge(home_assistant_api); for &route in SPA_ROUTES { app = app.route(route, get(index)); } let app = app.fallback(not_found); let app = if state.config.base_path.is_empty() { app } else { let base = state.config.base_path.clone(); let redirect_to = format!("{base}/"); Router::new() .route(&base, get(move || { let redirect_to = redirect_to.clone(); async move { Redirect::permanent(&redirect_to) } })) .nest(&base, app) .fallback(not_found) }; app .layer(CompressionLayer::new()) .layer(TraceLayer::new_for_http()) .layer(middleware::from_fn(security_headers)) .layer(middleware::from_fn_with_state(state.clone(), debug_api_requests)) .with_state(state) } // Functional source split intentionally keeps items in the existing module namespace. include!("api/auth.rs"); include!("api/system.rs"); include!("api/devices.rs"); include!("api/zones.rs"); include!("api/groups.rs"); include!("api/house.rs"); include!("api/schedules.rs"); include!("api/automations.rs"); include!("api/flows.rs"); include!("api/history.rs"); include!("api/events.rs"); include!("api/settings.rs"); include!("api/debug_tokens.rs"); include!("api/integrations.rs"); include!("api/middleware.rs"); include!("api/public_settings.rs"); include!("api/websocket.rs"); include!("api/assets.rs");