190 lines
8.5 KiB
Rust
190 lines
8.5 KiB
Rust
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");
|