From 63bdeed9529fe3f34686d9c9bf9b53805b944118 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 30 Sep 2024 22:11:48 +0200 Subject: [PATCH 1/6] Add function to report devices activity --- central_backend/Cargo.lock | 11 +++ central_backend/Cargo.toml | 3 +- central_backend/src/app_config.rs | 10 ++ central_backend/src/lib.rs | 1 + central_backend/src/logs/log_entry.rs | 17 ++++ central_backend/src/logs/logs_manager.rs | 41 ++++++++ central_backend/src/logs/mod.rs | 3 + central_backend/src/logs/severity.rs | 7 ++ central_backend/src/main.rs | 1 + .../devices_api/device_logging_controller.rs | 22 +++++ .../src/server/devices_api/jwt_parser.rs | 96 +++++++++++++++++++ .../src/server/devices_api/mgmt_controller.rs | 74 +------------- central_backend/src/server/devices_api/mod.rs | 2 + central_backend/src/server/servers.rs | 6 +- central_backend/src/utils/time_utils.rs | 5 + python_device/src/api.py | 31 ++++-- python_device/src/main.py | 15 ++- 17 files changed, 266 insertions(+), 79 deletions(-) create mode 100644 central_backend/src/logs/log_entry.rs create mode 100644 central_backend/src/logs/logs_manager.rs create mode 100644 central_backend/src/logs/mod.rs create mode 100644 central_backend/src/logs/severity.rs create mode 100644 central_backend/src/server/devices_api/device_logging_controller.rs create mode 100644 central_backend/src/server/devices_api/jwt_parser.rs diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index f1408bd..77b2c81 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "clap", "env_logger", "foreign-types-shared", + "fs4", "futures", "futures-util", "jsonwebtoken", @@ -1021,6 +1022,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c6b3bd49c37d2aa3f3f2220233b29a7cd23f79d1fe70e5337d25fb390793de" +dependencies = [ + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "futures" version = "0.3.30" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index 44cd8da..b79a99a 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -38,4 +38,5 @@ jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } prettytable-rs = "0.10.0" chrono = "0.4.38" serde_yml = "0.0.12" -bincode = "=2.0.0-rc.3" \ No newline at end of file +bincode = "=2.0.0-rc.3" +fs4 = { version = "0.9", features = ["sync"] } \ No newline at end of file diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index 09a7130..f443418 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -283,6 +283,16 @@ impl AppConfig { } )) } + + /// Get logs directory + pub fn logs_dir(&self) -> PathBuf { + self.storage_path().join("logs") + } + + /// Get the logs for a given day + pub fn log_of_day(&self, day: u64) -> PathBuf { + self.logs_dir().join(format!("{day}.log")) + } } #[cfg(test)] diff --git a/central_backend/src/lib.rs b/central_backend/src/lib.rs index 16c1e6c..c043b60 100644 --- a/central_backend/src/lib.rs +++ b/central_backend/src/lib.rs @@ -3,5 +3,6 @@ pub mod constants; pub mod crypto; pub mod devices; pub mod energy; +pub mod logs; pub mod server; pub mod utils; diff --git a/central_backend/src/logs/log_entry.rs b/central_backend/src/logs/log_entry.rs new file mode 100644 index 0000000..d20ea55 --- /dev/null +++ b/central_backend/src/logs/log_entry.rs @@ -0,0 +1,17 @@ +use crate::devices::device::DeviceId; +use crate::logs::severity::LogSeverity; + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct LogEntry { + /// If no device is specified then the message comes from the backend + pub device_id: Option, + pub time: u64, + pub severity: LogSeverity, + pub message: String, +} + +impl LogEntry { + pub fn serialize(&self) -> anyhow::Result { + Ok(serde_json::to_string(self)?) + } +} diff --git a/central_backend/src/logs/logs_manager.rs b/central_backend/src/logs/logs_manager.rs new file mode 100644 index 0000000..ba3cb8a --- /dev/null +++ b/central_backend/src/logs/logs_manager.rs @@ -0,0 +1,41 @@ +use crate::app_config::AppConfig; +use crate::devices::device::DeviceId; +use crate::logs::log_entry::LogEntry; +use crate::logs::severity::LogSeverity; +use crate::utils::time_utils::{curr_day_number, time_secs}; +use fs4::fs_std::FileExt; +use std::fs::OpenOptions; +use std::io::{Seek, SeekFrom, Write}; + +pub fn save_log( + device: Option<&DeviceId>, + severity: LogSeverity, + message: String, +) -> anyhow::Result<()> { + let log_path = AppConfig::get().log_of_day(curr_day_number()); + + let mut file = OpenOptions::new() + .append(true) + .create(true) + .open(&log_path)?; + + file.lock_exclusive()?; + file.seek(SeekFrom::End(0))?; + file.write_all( + format!( + "{}\n", + (LogEntry { + device_id: device.cloned(), + time: time_secs(), + severity, + message, + }) + .serialize()? + ) + .as_bytes(), + )?; + file.flush()?; + file.unlock()?; + + Ok(()) +} diff --git a/central_backend/src/logs/mod.rs b/central_backend/src/logs/mod.rs new file mode 100644 index 0000000..a07def6 --- /dev/null +++ b/central_backend/src/logs/mod.rs @@ -0,0 +1,3 @@ +pub mod log_entry; +pub mod logs_manager; +pub mod severity; diff --git a/central_backend/src/logs/severity.rs b/central_backend/src/logs/severity.rs new file mode 100644 index 0000000..9f461c2 --- /dev/null +++ b/central_backend/src/logs/severity.rs @@ -0,0 +1,7 @@ +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] +pub enum LogSeverity { + Debug, + Info, + Warn, + Error, +} diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index f910784..23fbac9 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -19,6 +19,7 @@ async fn main() -> std::io::Result<()> { create_directory_if_missing(AppConfig::get().devices_config_path()).unwrap(); create_directory_if_missing(AppConfig::get().relays_runtime_stats_storage_path()).unwrap(); create_directory_if_missing(AppConfig::get().energy_consumption_history()).unwrap(); + create_directory_if_missing(AppConfig::get().logs_dir()).unwrap(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!"); diff --git a/central_backend/src/server/devices_api/device_logging_controller.rs b/central_backend/src/server/devices_api/device_logging_controller.rs new file mode 100644 index 0000000..1387b33 --- /dev/null +++ b/central_backend/src/server/devices_api/device_logging_controller.rs @@ -0,0 +1,22 @@ +use crate::logs::logs_manager; +use crate::logs::severity::LogSeverity; +use crate::server::custom_error::HttpResult; +use crate::server::devices_api::jwt_parser::JWTRequest; +use crate::server::WebEnergyActor; +use actix_web::{web, HttpResponse}; + +#[derive(Debug, serde::Deserialize)] +pub struct LogRequest { + severity: LogSeverity, + message: String, +} + +/// Report log message from device +pub async fn report_log(body: web::Json, actor: WebEnergyActor) -> HttpResult { + let (device, request) = body.parse_jwt::(actor).await?; + + log::info!("Save log message from device: {request:#?}"); + logs_manager::save_log(Some(&device.id), request.severity, request.message)?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/central_backend/src/server/devices_api/jwt_parser.rs b/central_backend/src/server/devices_api/jwt_parser.rs new file mode 100644 index 0000000..e135c13 --- /dev/null +++ b/central_backend/src/server/devices_api/jwt_parser.rs @@ -0,0 +1,96 @@ +use crate::app_config::AppConfig; +use crate::crypto::pki; +use crate::devices::device::{Device, DeviceId}; +use crate::energy::energy_actor; +use crate::server::WebEnergyActor; +use jsonwebtoken::{Algorithm, DecodingKey, Validation}; +use openssl::x509::X509; +use serde::de::DeserializeOwned; +use std::collections::HashSet; + +#[derive(thiserror::Error, Debug)] +pub enum JWTError { + #[error("Failed to decode JWT header")] + FailedDecodeJWT, + #[error("Missing KID in JWT!")] + MissingKidInJWT, + #[error("Sent a JWT for a device which does not exists!")] + DeviceDoesNotExists, + #[error("Sent a JWT for a device which is not validated!")] + DeviceNotValidated, + #[error("Sent a JWT using a revoked certificate!")] + RevokedCertificate, + #[error("Failed to validate JWT!")] + FailedValidateJWT, +} + +#[derive(serde::Deserialize)] +pub struct JWTRequest { + pub payload: String, +} + +impl JWTRequest { + pub async fn parse_jwt( + &self, + actor: WebEnergyActor, + ) -> anyhow::Result<(Device, E)> { + // First, we need to extract device kid from query + let Ok(jwt_header) = jsonwebtoken::decode_header(&self.payload) else { + log::error!("Failed to decode JWT header!"); + return Err(JWTError::FailedDecodeJWT.into()); + }; + + let Some(kid) = jwt_header.kid else { + log::error!("Missing KID in JWT!"); + return Err(JWTError::MissingKidInJWT.into()); + }; + + // Fetch device information + let Some(device) = actor + .send(energy_actor::GetSingleDevice(DeviceId(kid))) + .await? + else { + log::error!("Sent a JWT for a device which does not exists!"); + return Err(JWTError::DeviceDoesNotExists.into()); + }; + + if !device.validated { + log::error!("Sent a JWT for a device which is not validated!"); + return Err(JWTError::DeviceNotValidated.into()); + } + + // Check certificate revocation status + let cert_bytes = std::fs::read(AppConfig::get().device_cert_path(&device.id))?; + let certificate = X509::from_pem(&cert_bytes)?; + + if pki::CertData::load_devices_ca()?.is_revoked(&certificate)? { + log::error!("Sent a JWT using a revoked certificate!"); + return Err(JWTError::RevokedCertificate.into()); + } + + let (key, alg) = match DecodingKey::from_ec_pem(&cert_bytes) { + Ok(key) => (key, Algorithm::ES256), + Err(e) => { + log::warn!("Failed to decode certificate as EC certificate {e}, trying RSA..."); + ( + DecodingKey::from_rsa_pem(&cert_bytes) + .expect("Failed to decode RSA certificate"), + Algorithm::RS256, + ) + } + }; + let mut validation = Validation::new(alg); + validation.validate_exp = false; + validation.required_spec_claims = HashSet::default(); + + let c = match jsonwebtoken::decode::(&self.payload, &key, &validation) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to validate JWT! {e}"); + return Err(JWTError::FailedValidateJWT.into()); + } + }; + + Ok((device, c.claims)) + } +} diff --git a/central_backend/src/server/devices_api/mgmt_controller.rs b/central_backend/src/server/devices_api/mgmt_controller.rs index 3c201eb..835486c 100644 --- a/central_backend/src/server/devices_api/mgmt_controller.rs +++ b/central_backend/src/server/devices_api/mgmt_controller.rs @@ -1,15 +1,13 @@ use crate::app_config::AppConfig; -use crate::crypto::pki; use crate::devices::device::{DeviceId, DeviceInfo}; use crate::energy::energy_actor; use crate::energy::energy_actor::RelaySyncStatus; use crate::server::custom_error::HttpResult; +use crate::server::devices_api::jwt_parser::JWTRequest; use crate::server::WebEnergyActor; use actix_web::{web, HttpResponse}; -use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use openssl::nid::Nid; -use openssl::x509::{X509Req, X509}; -use std::collections::HashSet; +use openssl::x509::X509Req; #[derive(Debug, serde::Deserialize)] pub struct EnrollRequest { @@ -129,11 +127,6 @@ pub async fn get_certificate(query: web::Query, actor: WebEnergyAc .body(cert)) } -#[derive(serde::Deserialize)] -pub struct SyncRequest { - payload: String, -} - #[derive(Debug, serde::Serialize, serde::Deserialize)] struct Claims { info: DeviceInfo, @@ -145,68 +138,11 @@ struct SyncResult { } /// Synchronize device -pub async fn sync_device(body: web::Json, actor: WebEnergyActor) -> HttpResult { - // First, we need to extract device kid from query - let Ok(jwt_header) = jsonwebtoken::decode_header(&body.payload) else { - log::error!("Failed to decode JWT header!"); - return Ok(HttpResponse::BadRequest().json("Failed to decode JWT header!")); - }; - - let Some(kid) = jwt_header.kid else { - log::error!("Missing KID in JWT!"); - return Ok(HttpResponse::BadRequest().json("Missing KID in JWT!")); - }; - - // Fetch device information - let Some(device) = actor - .send(energy_actor::GetSingleDevice(DeviceId(kid))) - .await? - else { - log::error!("Sent a JWT for a device which does not exists!"); - return Ok(HttpResponse::NotFound().json("Sent a JWT for a device which does not exists!")); - }; - - if !device.validated { - log::error!("Sent a JWT for a device which is not validated!"); - return Ok(HttpResponse::PreconditionFailed() - .json("Sent a JWT for a device which is not validated!")); - } - - // Check certificate revocation status - let cert_bytes = std::fs::read(AppConfig::get().device_cert_path(&device.id))?; - let certificate = X509::from_pem(&cert_bytes)?; - - if pki::CertData::load_devices_ca()?.is_revoked(&certificate)? { - log::error!("Sent a JWT using a revoked certificate!"); - return Ok( - HttpResponse::PreconditionFailed().json("Sent a JWT using a revoked certificate!") - ); - } - - let (key, alg) = match DecodingKey::from_ec_pem(&cert_bytes) { - Ok(key) => (key, Algorithm::ES256), - Err(e) => { - log::warn!("Failed to decode certificate as EC certificate {e}, trying RSA..."); - ( - DecodingKey::from_rsa_pem(&cert_bytes).expect("Failed to decode RSA certificate"), - Algorithm::RS256, - ) - } - }; - let mut validation = Validation::new(alg); - validation.validate_exp = false; - validation.required_spec_claims = HashSet::default(); - - let c = match jsonwebtoken::decode::(&body.payload, &key, &validation) { - Ok(c) => c, - Err(e) => { - log::error!("Failed to validate JWT! {e}"); - return Ok(HttpResponse::PreconditionFailed().json("Failed to validate JWT!")); - } - }; +pub async fn sync_device(body: web::Json, actor: WebEnergyActor) -> HttpResult { + let (device, claims) = body.0.parse_jwt::(actor.clone()).await?; let relays = actor - .send(energy_actor::SynchronizeDevice(device.id, c.claims.info)) + .send(energy_actor::SynchronizeDevice(device.id, claims.info)) .await??; Ok(HttpResponse::Ok().json(SyncResult { relays })) diff --git a/central_backend/src/server/devices_api/mod.rs b/central_backend/src/server/devices_api/mod.rs index 416e535..58dcde0 100644 --- a/central_backend/src/server/devices_api/mod.rs +++ b/central_backend/src/server/devices_api/mod.rs @@ -1,2 +1,4 @@ +pub mod device_logging_controller; +pub mod jwt_parser; pub mod mgmt_controller; pub mod utils_controller; diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index c0325f3..46d54ed 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -3,7 +3,7 @@ use crate::constants; use crate::crypto::pki; use crate::energy::energy_actor::EnergyActorAddr; use crate::server::auth_middleware::AuthChecker; -use crate::server::devices_api::{mgmt_controller, utils_controller}; +use crate::server::devices_api::{device_logging_controller, mgmt_controller, utils_controller}; use crate::server::unsecure_server::*; use crate::server::web_api::*; use crate::server::web_app_controller; @@ -226,6 +226,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/devices_api/mgmt/sync", web::post().to(mgmt_controller::sync_device), ) + .route( + "/devices_api/logging/record", + web::post().to(device_logging_controller::report_log), + ) // Web app .route("/", web::get().to(web_app_controller::root_index)) .route( diff --git a/central_backend/src/utils/time_utils.rs b/central_backend/src/utils/time_utils.rs index 91828a6..acdc994 100644 --- a/central_backend/src/utils/time_utils.rs +++ b/central_backend/src/utils/time_utils.rs @@ -22,6 +22,11 @@ pub fn day_number(time: u64) -> u64 { time / (3600 * 24) } +/// Get current day number +pub fn curr_day_number() -> u64 { + day_number(time_secs()) +} + /// Get current hour, 00 => 23 (local time) pub fn curr_hour() -> u32 { let local: DateTime = Local::now(); diff --git a/python_device/src/api.py b/python_device/src/api.py index e06c9a3..29328c3 100644 --- a/python_device/src/api.py +++ b/python_device/src/api.py @@ -4,6 +4,7 @@ import src.constants as constants from cryptography.x509 import load_pem_x509_certificate from cryptography import utils import jwt +import json def get_secure_origin() -> str: @@ -75,13 +76,18 @@ def device_certificate() -> str: return res.text +def jwt_sign(data: any, dev_id: str, privkey) -> str: + """ + Generate a JWT for client request + """ + return jwt.encode(data, privkey, algorithm="RS256", headers={"kid": dev_id}) + + def sync_device(dev_id: str, privkey): """ Synchronize device with backend """ - encoded = jwt.encode( - {"info": device_info()}, privkey, algorithm="RS256", headers={"kid": dev_id} - ) + encoded = jwt_sign({"info": device_info()}, dev_id=dev_id, privkey=privkey) res = requests.post( f"{args.secure_origin}/devices_api/mgmt/sync", @@ -89,6 +95,19 @@ def sync_device(dev_id: str, privkey): verify=args.root_ca_path, ) - print(encoded) - print(res) - print(res.text) + return json.loads(res.text) + + +def report_log(severity: str, message: str, dev_id: str, privkey): + """ + Report log message to server + """ + encoded = jwt_sign( + {"severity": severity, "message": message}, dev_id=dev_id, privkey=privkey + ) + + requests.post( + f"{args.secure_origin}/devices_api/logging/record", + json={"payload": encoded}, + verify=args.root_ca_path, + ) diff --git a/python_device/src/main.py b/python_device/src/main.py index a1c62fb..b81a07f 100644 --- a/python_device/src/main.py +++ b/python_device/src/main.py @@ -3,6 +3,10 @@ import src.api as api import src.pki as pki import src.utils as utils import os +import time + +# TODO : turn off all relays +# TODO : intialize GPIO print("Check storage") if not os.path.isdir(args.storage): @@ -88,6 +92,13 @@ if not os.path.isfile(args.dev_crt_path): with open(args.dev_crt_path, "w") as f: f.write(cert) +api.report_log("Info", "Starting program main loop...", args.dev_id, args.priv_key) -print("Done. ready to operate.") -api.sync_device(args.dev_id, args.priv_key) +print("Ready to operate!.") +while True: + + # TODO : implement this loop more properly + res = api.sync_device(args.dev_id, args.priv_key) + print(res) + + time.sleep(5) From 75753051f9463239e08ae8d9cd6caf3c183da45f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 1 Oct 2024 22:27:34 +0200 Subject: [PATCH 2/6] Add function to extract logs --- central_backend/src/logs/logs_manager.rs | 17 +++++++++++ central_backend/src/logs/severity.rs | 4 +-- central_backend/src/server/servers.rs | 5 ++++ .../src/server/web_api/logging_controller.rs | 30 +++++++++++++++++++ central_backend/src/server/web_api/mod.rs | 1 + 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 central_backend/src/server/web_api/logging_controller.rs diff --git a/central_backend/src/logs/logs_manager.rs b/central_backend/src/logs/logs_manager.rs index ba3cb8a..fb0d1f8 100644 --- a/central_backend/src/logs/logs_manager.rs +++ b/central_backend/src/logs/logs_manager.rs @@ -39,3 +39,20 @@ pub fn save_log( Ok(()) } + +/// Make a logs extraction +pub fn get_logs(day: u64) -> anyhow::Result> { + let file = AppConfig::get().log_of_day(day); + + if !file.exists() { + return Ok(Vec::new()); + } + + let content = std::fs::read_to_string(file)? + .split('\n') + .filter(|l| !l.is_empty()) + .map(serde_json::from_str) + .collect::, _>>()?; + + Ok(content) +} diff --git a/central_backend/src/logs/severity.rs b/central_backend/src/logs/severity.rs index 9f461c2..6265bd3 100644 --- a/central_backend/src/logs/severity.rs +++ b/central_backend/src/logs/severity.rs @@ -1,6 +1,6 @@ -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug, PartialOrd, Eq, PartialEq)] pub enum LogSeverity { - Debug, + Debug = 0, Info, Warn, Error, diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 46d54ed..44b0a6d 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -180,6 +180,11 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/device/{id}", web::delete().to(devices_controller::delete_device), ) + // Logging controller API + .route( + "/web_api/logging/logs", + web::get().to(logging_controller::get_log), + ) // Relays API .route( "/web_api/relays/list", diff --git a/central_backend/src/server/web_api/logging_controller.rs b/central_backend/src/server/web_api/logging_controller.rs new file mode 100644 index 0000000..dc48e32 --- /dev/null +++ b/central_backend/src/server/web_api/logging_controller.rs @@ -0,0 +1,30 @@ +use crate::devices::device::DeviceId; +use crate::logs::logs_manager; +use crate::logs::severity::LogSeverity; +use crate::server::custom_error::HttpResult; +use crate::utils::time_utils::curr_day_number; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Deserialize)] +pub struct LogRequest { + // Day number + day: Option, + min_severity: Option, + device: Option, +} + +/// Get some logs +pub async fn get_log(req: web::Query) -> HttpResult { + let day = req.day.unwrap_or_else(curr_day_number); + let mut logs = logs_manager::get_logs(day)?; + + if let Some(min_severity) = req.min_severity { + logs.retain(|d| d.severity >= min_severity); + } + + if let Some(dev_id) = &req.device { + logs.retain(|d| d.device_id.as_ref() == Some(dev_id)); + } + + Ok(HttpResponse::Ok().json(logs)) +} diff --git a/central_backend/src/server/web_api/mod.rs b/central_backend/src/server/web_api/mod.rs index eb48e75..5b75fab 100644 --- a/central_backend/src/server/web_api/mod.rs +++ b/central_backend/src/server/web_api/mod.rs @@ -1,5 +1,6 @@ pub mod auth_controller; pub mod devices_controller; pub mod energy_controller; +pub mod logging_controller; pub mod relays_controller; pub mod server_controller; From 7dfb172aeb901dddd63af69b51036c79348d3fa0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 2 Oct 2024 21:54:54 +0200 Subject: [PATCH 3/6] Frontend requests log on backend --- central_frontend/src/App.tsx | 2 + central_frontend/src/api/LogsAPI.ts | 29 +++++++ central_frontend/src/routes/LogsRoute.tsx | 75 +++++++++++++++++++ .../src/widgets/SolarEnergyNavList.tsx | 13 +++- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 central_frontend/src/api/LogsAPI.ts create mode 100644 central_frontend/src/routes/LogsRoute.tsx diff --git a/central_frontend/src/App.tsx b/central_frontend/src/App.tsx index c724bb4..242b59a 100644 --- a/central_frontend/src/App.tsx +++ b/central_frontend/src/App.tsx @@ -10,6 +10,7 @@ import { DeviceRoute } from "./routes/DeviceRoute/DeviceRoute"; import { DevicesRoute } from "./routes/DevicesRoute"; import { HomeRoute } from "./routes/HomeRoute"; import { LoginRoute } from "./routes/LoginRoute"; +import { LogsRoute } from "./routes/LogsRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; import { PendingDevicesRoute } from "./routes/PendingDevicesRoute"; import { RelaysListRoute } from "./routes/RelaysListRoute"; @@ -27,6 +28,7 @@ export function App() { } /> } /> } /> + } /> } /> ) diff --git a/central_frontend/src/api/LogsAPI.ts b/central_frontend/src/api/LogsAPI.ts new file mode 100644 index 0000000..72438ba --- /dev/null +++ b/central_frontend/src/api/LogsAPI.ts @@ -0,0 +1,29 @@ +import { Dayjs } from "dayjs"; +import { APIClient } from "./ApiClient"; + +export type LogSeverity = "Debug" | "Info" | "Warn" | "Error"; + +export interface LogEntry { + device_id: string; + time: number; + severity: LogSeverity; + message: string; +} + +export class LogsAPI { + /** + * Request the logs from the server + * + * @param date The date that contains the requested date + */ + static async GetLogs(date: Dayjs): Promise { + const day = Math.floor(date.unix() / (3600 * 24)); + + const res = await APIClient.exec({ + uri: `/logging/logs?day=${day}`, + method: "GET", + }); + + return res.data; + } +} diff --git a/central_frontend/src/routes/LogsRoute.tsx b/central_frontend/src/routes/LogsRoute.tsx new file mode 100644 index 0000000..12c37f1 --- /dev/null +++ b/central_frontend/src/routes/LogsRoute.tsx @@ -0,0 +1,75 @@ +import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { IconButton, Tooltip } from "@mui/material"; +import { DatePicker } from "@mui/x-date-pickers"; +import dayjs from "dayjs"; +import React from "react"; +import { LogEntry, LogsAPI } from "../api/LogsAPI"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; + +export function LogsRoute(): React.ReactElement { + const loadKey = React.useRef(1); + + const [currDate, setCurrDate] = React.useState(dayjs()); + + const [logs, setLogs] = React.useState(); + + const load = async () => { + setLogs(await LogsAPI.GetLogs(currDate)); + }; + + const reload = () => { + setLogs(undefined); + loadKey.current += 1; + }; + return ( + + + + + + } + > +
+ + setCurrDate(currDate.add(-1, "day"))}> + + + + setCurrDate(d === null ? currDate : d)} + /> + + setCurrDate(currDate.add(1, "day"))}> + + + +
+ } + /> +
+ ); +} + +function LogsView(p: { logs: LogEntry[] }): React.ReactElement { + return "TODO : show logs"; +} diff --git a/central_frontend/src/widgets/SolarEnergyNavList.tsx b/central_frontend/src/widgets/SolarEnergyNavList.tsx index bf180c6..f5546fb 100644 --- a/central_frontend/src/widgets/SolarEnergyNavList.tsx +++ b/central_frontend/src/widgets/SolarEnergyNavList.tsx @@ -1,4 +1,10 @@ -import { mdiChip, mdiElectricSwitch, mdiHome, mdiNewBox } from "@mdi/js"; +import { + mdiChip, + mdiElectricSwitch, + mdiHome, + mdiNewBox, + mdiNotebookMultiple, +} from "@mdi/js"; import Icon from "@mdi/react"; import { List, @@ -35,6 +41,11 @@ export function SolarEnergyNavList(): React.ReactElement { uri="/relays" icon={} /> + } + /> ); } From caf05d9126af1421d23beea401a10f7c3695327f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 3 Oct 2024 20:52:05 +0200 Subject: [PATCH 4/6] Display log events --- central_frontend/src/routes/LogsRoute.tsx | 51 ++++++++++++++++++- .../src/widgets/SolarEnergyRouteContainer.tsx | 9 +++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/central_frontend/src/routes/LogsRoute.tsx b/central_frontend/src/routes/LogsRoute.tsx index 12c37f1..192264e 100644 --- a/central_frontend/src/routes/LogsRoute.tsx +++ b/central_frontend/src/routes/LogsRoute.tsx @@ -1,7 +1,18 @@ import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import RefreshIcon from "@mui/icons-material/Refresh"; -import { IconButton, Tooltip } from "@mui/material"; +import { + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@mui/material"; import { DatePicker } from "@mui/x-date-pickers"; import dayjs from "dayjs"; import React from "react"; @@ -41,6 +52,7 @@ export function LogsRoute(): React.ReactElement { display: "flex", alignItems: "center", justifyContent: "center", + marginBottom: "10px", }} > @@ -71,5 +83,40 @@ export function LogsRoute(): React.ReactElement { } function LogsView(p: { logs: LogEntry[] }): React.ReactElement { - return "TODO : show logs"; + if (p.logs.length == 0) { + return ( + + There was no log recorded on this day. + + ); + } + + return ( + + + + + Device ID + Time + Severity + Message + + + + {p.logs.map((row, id) => ( + + + {row.device_id ?? "Backend"} + + + {new Date(row.time * 1000).toLocaleTimeString()} + + {row.severity} + {row.message} + + ))} + +
+
+ ); } diff --git a/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx b/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx index 5b87fee..80a3cc3 100644 --- a/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx +++ b/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx @@ -9,13 +9,20 @@ export function SolarEnergyRouteContainer( } & PropsWithChildren ): React.ReactElement { return ( -
+
{p.label} From 436bcd5677f8a6a3a89e48ee7e739c77fefa62ee Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 3 Oct 2024 21:01:38 +0200 Subject: [PATCH 5/6] Reverse display order of log messages --- central_frontend/src/routes/LogsRoute.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/central_frontend/src/routes/LogsRoute.tsx b/central_frontend/src/routes/LogsRoute.tsx index 192264e..89870ed 100644 --- a/central_frontend/src/routes/LogsRoute.tsx +++ b/central_frontend/src/routes/LogsRoute.tsx @@ -28,7 +28,9 @@ export function LogsRoute(): React.ReactElement { const [logs, setLogs] = React.useState(); const load = async () => { - setLogs(await LogsAPI.GetLogs(currDate)); + const logs = await LogsAPI.GetLogs(currDate); + logs.reverse(); + setLogs(logs); }; const reload = () => { From 06659404c1fe5cab353b911ece373d01a6f561f9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 3 Oct 2024 21:16:51 +0200 Subject: [PATCH 6/6] Prepare the path for OTA implementation --- central_backend/src/app_config.rs | 10 ++++++++++ central_backend/src/main.rs | 1 + central_backend/src/server/servers.rs | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index f443418..b2a21e8 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -293,6 +293,16 @@ impl AppConfig { pub fn log_of_day(&self, day: u64) -> PathBuf { self.logs_dir().join(format!("{day}.log")) } + + /// Get the directory that will store OTA updates + pub fn ota_dir(&self) -> PathBuf { + self.logs_dir().join("ota") + } + + /// Get the directory that will store OTA updates of a given device reference + pub fn ota_of_device(&self, dev_ref: &str) -> PathBuf { + self.ota_dir().join(dev_ref) + } } #[cfg(test)] diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index 23fbac9..76de370 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -20,6 +20,7 @@ async fn main() -> std::io::Result<()> { create_directory_if_missing(AppConfig::get().relays_runtime_stats_storage_path()).unwrap(); create_directory_if_missing(AppConfig::get().energy_consumption_history()).unwrap(); create_directory_if_missing(AppConfig::get().logs_dir()).unwrap(); + create_directory_if_missing(AppConfig::get().ota_dir()).unwrap(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!"); diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 44b0a6d..39ba213 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -180,6 +180,13 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/device/{id}", web::delete().to(devices_controller::delete_device), ) + // OTA API + // TODO : list supported platform references + // TODO : upload a new software update + // TODO : list ota software update per platform + // TODO : download a OTA file + // TODO : delete an OTA file + // TODO : deploy an update to a device // Logging controller API .route( "/web_api/logging/logs",