Add authentication layer
This commit is contained in:
parent
738c53c8b9
commit
e1739d9818
214
central_backend/Cargo.lock
generated
214
central_backend/Cargo.lock
generated
@ -44,6 +44,21 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-cors"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331"
|
||||||
|
dependencies = [
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"derive_more",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.8.0"
|
version = "3.8.0"
|
||||||
@ -56,7 +71,7 @@ dependencies = [
|
|||||||
"actix-tls",
|
"actix-tls",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"ahash",
|
"ahash",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -84,6 +99,22 @@ dependencies = [
|
|||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-identity"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2c99b7a5614b72a78f04aa2021e5370fc1aef2475fffeffc0c1266b99007062"
|
||||||
|
dependencies = [
|
||||||
|
"actix-service",
|
||||||
|
"actix-session",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"derive_more",
|
||||||
|
"futures-core",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-macros"
|
name = "actix-macros"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@ -94,6 +125,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-remote-ip"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7629b357d4705cf3f1e31f989f48ecd56027112f7d52dcf06dd96ee197065f8e"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@ -147,6 +189,22 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-session"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2"
|
||||||
|
dependencies = [
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"anyhow",
|
||||||
|
"derive_more",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-tls"
|
name = "actix-tls"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@ -256,6 +314,41 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
@ -395,6 +488,12 @@ dependencies = [
|
|||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@ -480,6 +579,10 @@ name = "central_backend"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
|
"actix-cors",
|
||||||
|
"actix-identity",
|
||||||
|
"actix-remote-ip",
|
||||||
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"asn1",
|
"asn1",
|
||||||
@ -487,6 +590,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared",
|
||||||
"futures",
|
"futures",
|
||||||
|
"futures-util",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@ -505,6 +609,16 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.7"
|
version = "4.5.7"
|
||||||
@ -563,7 +677,14 @@ version = "0.16.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
|
"base64 0.20.0",
|
||||||
|
"hkdf",
|
||||||
|
"hmac",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"rand",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
@ -624,9 +745,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@ -657,6 +788,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -863,6 +995,16 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
@ -919,6 +1061,24 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hkdf"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -1081,6 +1241,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@ -1245,6 +1414,12 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.64"
|
version = "0.10.64"
|
||||||
@ -1362,6 +1537,18 @@ version = "0.3.30"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1472,7 +1659,7 @@ version = "0.12.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
|
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -1571,7 +1758,7 @@ version = "2.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1696,6 +1883,17 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@ -2007,6 +2205,16 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -22,3 +22,8 @@ reqwest = "0.12.5"
|
|||||||
serde_json = "1.0.118"
|
serde_json = "1.0.118"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
actix = "0.13.5"
|
actix = "0.13.5"
|
||||||
|
actix-identity = "0.7.1"
|
||||||
|
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||||
|
actix-cors = "0.7.0"
|
||||||
|
actix-remote-ip = "0.1.0"
|
||||||
|
futures-util = "0.3.30"
|
@ -23,6 +23,32 @@ pub enum ConsumptionBackend {
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
|
/// Proxy IP, might end with a star "*"
|
||||||
|
#[clap(short, long, env)]
|
||||||
|
pub proxy_ip: Option<String>,
|
||||||
|
|
||||||
|
/// Secret key, used to sign some resources. Must be randomly generated
|
||||||
|
#[clap(short = 'S', long, env, default_value = "")]
|
||||||
|
secret: String,
|
||||||
|
|
||||||
|
/// Specify whether the cookie should be transmitted only over secure connections
|
||||||
|
///
|
||||||
|
/// This should be always true when running in production mode
|
||||||
|
#[clap(long, env)]
|
||||||
|
pub cookie_secure: bool,
|
||||||
|
|
||||||
|
/// Unsecure : for development, bypass authentication
|
||||||
|
#[clap(long, env)]
|
||||||
|
pub unsecure_disable_login: bool,
|
||||||
|
|
||||||
|
/// Admin username
|
||||||
|
#[clap(long, env, default_value = "admin")]
|
||||||
|
pub admin_username: String,
|
||||||
|
|
||||||
|
/// Admin password
|
||||||
|
#[clap(long, env, default_value = "admin")]
|
||||||
|
pub admin_password: String,
|
||||||
|
|
||||||
/// The port the server will listen to (using HTTPS)
|
/// The port the server will listen to (using HTTPS)
|
||||||
#[arg(short, long, env, default_value = "0.0.0.0:8443")]
|
#[arg(short, long, env, default_value = "0.0.0.0:8443")]
|
||||||
pub listen_address: String,
|
pub listen_address: String,
|
||||||
@ -56,6 +82,21 @@ impl AppConfig {
|
|||||||
&ARGS
|
&ARGS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get app secret
|
||||||
|
pub fn secret(&self) -> &str {
|
||||||
|
let mut secret = self.secret.as_str();
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) && secret.is_empty() {
|
||||||
|
secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY";
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret.is_empty() {
|
||||||
|
panic!("SECRET is undefined or too short (min 64 chars)!")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
|
||||||
/// URL for unsecure connections
|
/// URL for unsecure connections
|
||||||
pub fn unsecure_origin(&self) -> String {
|
pub fn unsecure_origin(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
@ -74,6 +115,23 @@ impl AppConfig {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get auth cookie domain
|
||||||
|
pub fn cookie_domain(&self) -> Option<String> {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let domain = self.secure_origin().split_once("://")?.1.to_string();
|
||||||
|
Some(
|
||||||
|
domain
|
||||||
|
.split_once(':')
|
||||||
|
.map(|s| s.0)
|
||||||
|
.unwrap_or(&domain)
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// In release mode, the web app is hosted on the same origin as the API
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get storage path
|
/// Get storage path
|
||||||
pub fn storage_path(&self) -> PathBuf {
|
pub fn storage_path(&self) -> PathBuf {
|
||||||
Path::new(&self.storage).to_path_buf()
|
Path::new(&self.storage).to_path_buf()
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Name of the cookie that contains session information
|
||||||
|
pub const SESSION_COOKIE_NAME: &str = "X-session-cookie";
|
||||||
|
|
||||||
/// Energy refresh operations interval
|
/// Energy refresh operations interval
|
||||||
pub const ENERGY_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
|
pub const ENERGY_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
/// Fallback value to use if production cannot be fetched
|
/// Fallback value to use if production cannot be fetched
|
||||||
pub const FALLBACK_PRODUCTION_VALUE: i32 = 5000;
|
pub const FALLBACK_PRODUCTION_VALUE: i32 = 5000;
|
||||||
|
|
||||||
|
/// Maximum session duration after inactivity, in seconds
|
||||||
|
pub const MAX_INACTIVITY_DURATION: u64 = 3600;
|
||||||
|
|
||||||
|
/// Maximum session duration (1 day)
|
||||||
|
pub const MAX_SESSION_DURATION: u64 = 3600 * 24;
|
||||||
|
|
||||||
|
/// List of routes that do not require authentication
|
||||||
|
pub const ROUTES_WITHOUT_AUTH: [&str; 2] =
|
||||||
|
["/web_api/server/config", "/web_api/auth/password_auth"];
|
||||||
|
@ -2,7 +2,7 @@ use actix::Actor;
|
|||||||
use central_backend::app_config::AppConfig;
|
use central_backend::app_config::AppConfig;
|
||||||
use central_backend::crypto::pki;
|
use central_backend::crypto::pki;
|
||||||
use central_backend::energy::energy_actor::EnergyActor;
|
use central_backend::energy::energy_actor::EnergyActor;
|
||||||
use central_backend::server::{secure_server, unsecure_server};
|
use central_backend::server::servers;
|
||||||
use central_backend::utils::files_utils::create_directory_if_missing;
|
use central_backend::utils::files_utils::create_directory_if_missing;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
|
|
||||||
@ -30,8 +30,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.expect("Failed to initialize energy actor!")
|
.expect("Failed to initialize energy actor!")
|
||||||
.start();
|
.start();
|
||||||
|
|
||||||
let s1 = secure_server(actor);
|
let s1 = servers::secure_server(actor);
|
||||||
let s2 = unsecure_server();
|
let s2 = servers::unsecure_server();
|
||||||
future::try_join(s1, s2)
|
future::try_join(s1, s2)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to start servers!");
|
.expect("Failed to start servers!");
|
||||||
|
97
central_backend/src/server/auth_middleware.rs
Normal file
97
central_backend/src/server/auth_middleware.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use actix_identity::Identity;
|
||||||
|
use std::future::{ready, Ready};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use actix_web::body::EitherBody;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::{
|
||||||
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
|
Error, FromRequest, HttpResponse,
|
||||||
|
};
|
||||||
|
use futures_util::future::LocalBoxFuture;
|
||||||
|
|
||||||
|
// There are two steps in middleware processing.
|
||||||
|
// 1. Middleware initialization, middleware factory gets called with
|
||||||
|
// next service in chain as parameter.
|
||||||
|
// 2. Middleware's call method gets called with normal request.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AuthChecker;
|
||||||
|
|
||||||
|
// Middleware factory is `Transform` trait
|
||||||
|
// `S` - type of the next service
|
||||||
|
// `B` - type of response's body
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for AuthChecker
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<EitherBody<B>>;
|
||||||
|
type Error = Error;
|
||||||
|
type Transform = AuthMiddleware<S>;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(AuthMiddleware {
|
||||||
|
service: Rc::new(service),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthMiddleware<S> {
|
||||||
|
service: Rc<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<EitherBody<B>>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let service = Rc::clone(&self.service);
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
// Check if no authentication is required
|
||||||
|
if constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
|
||||||
|
|| !req.path().starts_with("/web_api/")
|
||||||
|
{
|
||||||
|
log::trace!("No authentication is required")
|
||||||
|
}
|
||||||
|
// Dev only, check for auto login
|
||||||
|
else if AppConfig::get().unsecure_disable_login {
|
||||||
|
log::trace!("Authentication is disabled")
|
||||||
|
}
|
||||||
|
// Check cookie authentication
|
||||||
|
else {
|
||||||
|
let identity: Option<Identity> =
|
||||||
|
Identity::from_request(req.request(), &mut Payload::None)
|
||||||
|
.into_inner()
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if identity.is_none() {
|
||||||
|
log::error!(
|
||||||
|
"Missing identity information in request, user is not authenticated!"
|
||||||
|
);
|
||||||
|
return Ok(req
|
||||||
|
.into_response(HttpResponse::PreconditionFailed().finish())
|
||||||
|
.map_into_right_body());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
service
|
||||||
|
.call(req)
|
||||||
|
.await
|
||||||
|
.map(ServiceResponse::map_into_left_body)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -91,9 +91,22 @@ impl From<actix::MailboxError> for HttpErr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<actix_identity::error::GetIdentityError> for HttpErr {
|
||||||
|
fn from(value: actix_identity::error::GetIdentityError) -> Self {
|
||||||
|
HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<actix_identity::error::LoginError> for HttpErr {
|
||||||
|
fn from(value: actix_identity::error::LoginError) -> Self {
|
||||||
|
HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<HttpResponse> for HttpErr {
|
impl From<HttpResponse> for HttpErr {
|
||||||
fn from(value: HttpResponse) -> Self {
|
fn from(value: HttpResponse) -> Self {
|
||||||
HttpErr::HTTPResponse(value)
|
HttpErr::HTTPResponse(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
||||||
|
@ -1,70 +1,11 @@
|
|||||||
use actix_web::middleware::Logger;
|
use actix_web::web;
|
||||||
use actix_web::{web, App, HttpServer};
|
|
||||||
use openssl::ssl::{SslAcceptor, SslMethod};
|
|
||||||
|
|
||||||
use crate::app_config::AppConfig;
|
|
||||||
use crate::crypto::pki;
|
|
||||||
use crate::energy::energy_actor::EnergyActorAddr;
|
use crate::energy::energy_actor::EnergyActorAddr;
|
||||||
|
|
||||||
|
pub mod auth_middleware;
|
||||||
pub mod custom_error;
|
pub mod custom_error;
|
||||||
pub mod energy_controller;
|
pub mod servers;
|
||||||
pub mod pki_controller;
|
pub mod unsecure_server;
|
||||||
pub mod server_controller;
|
pub mod web_api;
|
||||||
|
|
||||||
pub type WebEnergyActor = web::Data<EnergyActorAddr>;
|
pub type WebEnergyActor = web::Data<EnergyActorAddr>;
|
||||||
|
|
||||||
/// Start unsecure (HTTP) server
|
|
||||||
pub async fn unsecure_server() -> anyhow::Result<()> {
|
|
||||||
log::info!(
|
|
||||||
"Unsecure server starting to listen on {} for {}",
|
|
||||||
AppConfig::get().unsecure_listen_address,
|
|
||||||
AppConfig::get().unsecure_origin()
|
|
||||||
);
|
|
||||||
HttpServer::new(|| {
|
|
||||||
App::new()
|
|
||||||
.wrap(Logger::default())
|
|
||||||
.route("/", web::get().to(server_controller::unsecure_home))
|
|
||||||
.route("/pki/{file}", web::get().to(pki_controller::serve_pki_file))
|
|
||||||
})
|
|
||||||
.bind(&AppConfig::get().unsecure_listen_address)?
|
|
||||||
.run()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start secure (HTTPS) server
|
|
||||||
pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> {
|
|
||||||
let web_ca = pki::CertData::load_web_ca()?;
|
|
||||||
let server_cert = pki::CertData::load_server()?;
|
|
||||||
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
|
||||||
builder.set_private_key(&server_cert.key)?;
|
|
||||||
builder.set_certificate(&server_cert.cert)?;
|
|
||||||
builder.add_extra_chain_cert(web_ca.cert)?;
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Secure server starting to listen on {} for {}",
|
|
||||||
AppConfig::get().listen_address,
|
|
||||||
AppConfig::get().secure_origin()
|
|
||||||
);
|
|
||||||
HttpServer::new(move || {
|
|
||||||
App::new()
|
|
||||||
.app_data(web::Data::new(energy_actor.clone()))
|
|
||||||
.wrap(Logger::default())
|
|
||||||
.route("/", web::get().to(server_controller::secure_home))
|
|
||||||
.route(
|
|
||||||
"/api/energy/curr_consumption",
|
|
||||||
web::get().to(energy_controller::curr_consumption),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/energy/cached_consumption",
|
|
||||||
web::get().to(energy_controller::cached_consumption),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.bind_openssl(&AppConfig::get().listen_address, builder)?
|
|
||||||
.run()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
134
central_backend/src/server/servers.rs
Normal file
134
central_backend/src/server/servers.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::crypto::pki;
|
||||||
|
use crate::energy::energy_actor::EnergyActorAddr;
|
||||||
|
use crate::server::auth_middleware::AuthChecker;
|
||||||
|
use crate::server::unsecure_server::*;
|
||||||
|
use crate::server::web_api::*;
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_identity::config::LogoutBehaviour;
|
||||||
|
use actix_identity::IdentityMiddleware;
|
||||||
|
use actix_remote_ip::RemoteIPConfig;
|
||||||
|
use actix_session::storage::CookieSessionStore;
|
||||||
|
use actix_session::SessionMiddleware;
|
||||||
|
use actix_web::cookie::{Key, SameSite};
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use openssl::ssl::{SslAcceptor, SslMethod};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Start unsecure (HTTP) server
|
||||||
|
pub async fn unsecure_server() -> anyhow::Result<()> {
|
||||||
|
log::info!(
|
||||||
|
"Unsecure server starting to listen on {} for {}",
|
||||||
|
AppConfig::get().unsecure_listen_address,
|
||||||
|
AppConfig::get().unsecure_origin()
|
||||||
|
);
|
||||||
|
HttpServer::new(|| {
|
||||||
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
web::get().to(unsecure_server_controller::unsecure_home),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/pki/{file}",
|
||||||
|
web::get().to(unsecure_pki_controller::serve_pki_file),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(&AppConfig::get().unsecure_listen_address)?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start secure (HTTPS) server
|
||||||
|
pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> {
|
||||||
|
let web_ca = pki::CertData::load_web_ca()?;
|
||||||
|
let server_cert = pki::CertData::load_server()?;
|
||||||
|
|
||||||
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
|
builder.set_private_key(&server_cert.key)?;
|
||||||
|
builder.set_certificate(&server_cert.cert)?;
|
||||||
|
builder.add_extra_chain_cert(web_ca.cert)?;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Secure server starting to listen on {} for {}",
|
||||||
|
AppConfig::get().listen_address,
|
||||||
|
AppConfig::get().secure_origin()
|
||||||
|
);
|
||||||
|
HttpServer::new(move || {
|
||||||
|
let session_mw = SessionMiddleware::builder(
|
||||||
|
CookieSessionStore::default(),
|
||||||
|
Key::from(AppConfig::get().secret().as_bytes()),
|
||||||
|
)
|
||||||
|
.cookie_name(constants::SESSION_COOKIE_NAME.to_string())
|
||||||
|
.cookie_secure(AppConfig::get().cookie_secure)
|
||||||
|
.cookie_same_site(SameSite::Strict)
|
||||||
|
.cookie_domain(AppConfig::get().cookie_domain())
|
||||||
|
.cookie_http_only(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let identity_middleware = IdentityMiddleware::builder()
|
||||||
|
.logout_behaviour(LogoutBehaviour::PurgeSession)
|
||||||
|
.visit_deadline(Some(Duration::from_secs(
|
||||||
|
constants::MAX_INACTIVITY_DURATION,
|
||||||
|
)))
|
||||||
|
.login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut cors = Cors::default()
|
||||||
|
.allowed_origin(&AppConfig::get().secure_origin())
|
||||||
|
.allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
||||||
|
.allowed_header("X-Auth-Token")
|
||||||
|
.allow_any_header()
|
||||||
|
.supports_credentials()
|
||||||
|
.max_age(3600);
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
cors = cors.allow_any_origin();
|
||||||
|
}
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.app_data(web::Data::new(energy_actor.clone()))
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.wrap(AuthChecker)
|
||||||
|
.wrap(identity_middleware)
|
||||||
|
.wrap(session_mw)
|
||||||
|
.wrap(cors)
|
||||||
|
.app_data(web::Data::new(RemoteIPConfig {
|
||||||
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
|
}))
|
||||||
|
.route("/", web::get().to(server_controller::secure_home))
|
||||||
|
.route(
|
||||||
|
"/web_api/server/config",
|
||||||
|
web::get().to(server_controller::config),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/auth/password_auth",
|
||||||
|
web::post().to(auth_controller::password_auth),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/auth/info",
|
||||||
|
web::get().to(auth_controller::auth_info),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/auth/sign_out",
|
||||||
|
web::get().to(auth_controller::sign_out),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/energy/curr_consumption",
|
||||||
|
web::get().to(energy_controller::curr_consumption),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/energy/cached_consumption",
|
||||||
|
web::get().to(energy_controller::cached_consumption),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind_openssl(&AppConfig::get().listen_address, builder)?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
2
central_backend/src/server/unsecure_server/mod.rs
Normal file
2
central_backend/src/server/unsecure_server/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod unsecure_pki_controller;
|
||||||
|
pub mod unsecure_server_controller;
|
@ -5,9 +5,3 @@ pub async fn unsecure_home() -> HttpResponse {
|
|||||||
.content_type("text/plain")
|
.content_type("text/plain")
|
||||||
.body("SolarEnergy unsecure central backend")
|
.body("SolarEnergy unsecure central backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn secure_home() -> HttpResponse {
|
|
||||||
HttpResponse::Ok()
|
|
||||||
.content_type("text/plain")
|
|
||||||
.body("SolarEnergy secure central backend")
|
|
||||||
}
|
|
52
central_backend/src/server/web_api/auth_controller.rs
Normal file
52
central_backend/src/server/web_api/auth_controller.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::server::custom_error::HttpResult;
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_remote_ip::RemoteIP;
|
||||||
|
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
user: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform password authentication
|
||||||
|
pub async fn password_auth(
|
||||||
|
r: web::Json<AuthRequest>,
|
||||||
|
request: HttpRequest,
|
||||||
|
remote_ip: RemoteIP,
|
||||||
|
) -> HttpResult {
|
||||||
|
if r.user != AppConfig::get().admin_username || r.password != AppConfig::get().admin_password {
|
||||||
|
log::error!("Failed login attempt from {}!", remote_ip.0.to_string());
|
||||||
|
return Ok(HttpResponse::Unauthorized().json("Invalid credentials!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Successful login attempt from {}!", remote_ip.0.to_string());
|
||||||
|
Identity::login(&request.extensions(), r.user.to_string())?;
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct AuthInfo {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current user information
|
||||||
|
pub async fn auth_info(id: Option<Identity>) -> HttpResult {
|
||||||
|
if AppConfig::get().unsecure_disable_login {
|
||||||
|
return Ok(HttpResponse::Ok().json(AuthInfo {
|
||||||
|
id: "auto login".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(AuthInfo {
|
||||||
|
id: id.unwrap().id()?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign out user
|
||||||
|
pub async fn sign_out(id: Identity) -> HttpResult {
|
||||||
|
id.logout();
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().finish())
|
||||||
|
}
|
3
central_backend/src/server/web_api/mod.rs
Normal file
3
central_backend/src/server/web_api/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod auth_controller;
|
||||||
|
pub mod energy_controller;
|
||||||
|
pub mod server_controller;
|
25
central_backend/src/server/web_api/server_controller.rs
Normal file
25
central_backend/src/server/web_api/server_controller.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
pub async fn secure_home() -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("text/plain")
|
||||||
|
.body("SolarEnergy secure central backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ServerConfig {
|
||||||
|
auth_disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
auth_disabled: AppConfig::get().unsecure_disable_login,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn config() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().json(ServerConfig::default())
|
||||||
|
}
|
1
central_frontend/.env
Normal file
1
central_frontend/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_BACKEND=https://localhost:8443/web_api
|
1
central_frontend/.env.production
Normal file
1
central_frontend/.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_BACKEND=/web_api
|
@ -1,5 +1,10 @@
|
|||||||
|
import { AuthApi } from "./api/AuthApi";
|
||||||
|
import { ServerApi } from "./api/ServerApi";
|
||||||
import { LoginRoute } from "./routes/LoginRoute";
|
import { LoginRoute } from "./routes/LoginRoute";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <LoginRoute />;
|
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
||||||
|
return <LoginRoute />;
|
||||||
|
|
||||||
|
return <>logged in todo</>;
|
||||||
}
|
}
|
||||||
|
177
central_frontend/src/api/ApiClient.ts
Normal file
177
central_frontend/src/api/ApiClient.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { AuthApi } from "./AuthApi";
|
||||||
|
|
||||||
|
interface RequestParams {
|
||||||
|
uri: string;
|
||||||
|
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
|
||||||
|
allowFail?: boolean;
|
||||||
|
jsonData?: any;
|
||||||
|
formData?: FormData;
|
||||||
|
upProgress?: (progress: number) => void;
|
||||||
|
downProgress?: (e: { progress: number; total: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APIResponse {
|
||||||
|
data: any;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(message: string, public code: number, public data: any) {
|
||||||
|
super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIClient {
|
||||||
|
/**
|
||||||
|
* Get backend URL
|
||||||
|
*/
|
||||||
|
static backendURL(): string {
|
||||||
|
const URL = import.meta.env.VITE_APP_BACKEND ?? "";
|
||||||
|
if (URL.length === 0) throw new Error("Backend URL undefined!");
|
||||||
|
return URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check out whether the backend is accessed through
|
||||||
|
* HTTPS or not
|
||||||
|
*/
|
||||||
|
static IsBackendSecure(): boolean {
|
||||||
|
return this.backendURL().startsWith("https");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a request on the backend
|
||||||
|
*/
|
||||||
|
static async exec(args: RequestParams): Promise<APIResponse> {
|
||||||
|
let body: string | undefined | FormData = undefined;
|
||||||
|
let headers: any = {};
|
||||||
|
|
||||||
|
// JSON request
|
||||||
|
if (args.jsonData) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
body = JSON.stringify(args.jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form data request
|
||||||
|
else if (args.formData) {
|
||||||
|
body = args.formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.backendURL() + args.uri;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
let status: number;
|
||||||
|
|
||||||
|
// Make the request with XMLHttpRequest
|
||||||
|
if (args.upProgress) {
|
||||||
|
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener("progress", (e) =>
|
||||||
|
args.upProgress!(e.loaded / e.total)
|
||||||
|
);
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("error", () =>
|
||||||
|
reject(new Error("File upload failed"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("abort", () =>
|
||||||
|
reject(new Error("File upload aborted"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("timeout", () =>
|
||||||
|
reject(new Error("File upload timeout"))
|
||||||
|
);
|
||||||
|
xhr.open(args.method, url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
for (const key in headers) {
|
||||||
|
if (headers.hasOwnProperty(key))
|
||||||
|
xhr.setRequestHeader(key, headers[key]);
|
||||||
|
}
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
if (res.responseType === "json") data = JSON.parse(res.responseText);
|
||||||
|
else data = res.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the request with fetch
|
||||||
|
else {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: args.method,
|
||||||
|
body: body,
|
||||||
|
headers: headers,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process response
|
||||||
|
// JSON response
|
||||||
|
if (res.headers.get("content-type") === "application/json")
|
||||||
|
data = await res.json();
|
||||||
|
// Text / XML response
|
||||||
|
else if (
|
||||||
|
["application/xml", "text/plain"].includes(
|
||||||
|
res.headers.get("content-type") ?? ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = await res.text();
|
||||||
|
// Binary file, tracking download progress
|
||||||
|
else if (res.body !== null && args.downProgress) {
|
||||||
|
// Track download progress
|
||||||
|
const contentEncoding = res.headers.get("content-encoding");
|
||||||
|
const contentLength = contentEncoding
|
||||||
|
? null
|
||||||
|
: res.headers.get("content-length");
|
||||||
|
|
||||||
|
const total = parseInt(contentLength ?? "0", 10);
|
||||||
|
let loaded = 0;
|
||||||
|
|
||||||
|
const resInt = new Response(
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const reader = res.body!.getReader();
|
||||||
|
|
||||||
|
const read = async () => {
|
||||||
|
try {
|
||||||
|
const ret = await reader.read();
|
||||||
|
if (ret.done) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loaded += ret.value.byteLength;
|
||||||
|
args.downProgress!({ progress: loaded, total });
|
||||||
|
controller.enqueue(ret.value);
|
||||||
|
read();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
controller.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
read();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
data = await resInt.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not track progress (binary file)
|
||||||
|
else data = await res.blob();
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle expired tokens
|
||||||
|
if (status === 412) {
|
||||||
|
AuthApi.UnsetAuthenticated();
|
||||||
|
window.location.href = import.meta.env.VITE_APP_BASENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.allowFail && (status < 200 || status > 299))
|
||||||
|
throw new ApiError("Request failed!", status, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
70
central_frontend/src/api/AuthApi.ts
Normal file
70
central_frontend/src/api/AuthApi.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface AuthInfo {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TokenStateKey = "auth-state";
|
||||||
|
|
||||||
|
export class AuthApi {
|
||||||
|
/**
|
||||||
|
* Check out whether user is signed in or not
|
||||||
|
*/
|
||||||
|
static get SignedIn(): boolean {
|
||||||
|
return localStorage.getItem(TokenStateKey) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark user as authenticated
|
||||||
|
*/
|
||||||
|
static SetAuthenticated() {
|
||||||
|
localStorage.setItem(TokenStateKey, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un-mark user as authenticated
|
||||||
|
*/
|
||||||
|
static UnsetAuthenticated() {
|
||||||
|
localStorage.removeItem(TokenStateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate using user and password
|
||||||
|
*/
|
||||||
|
static async AuthWithPassword(user: string, password: string): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/auth/password_auth",
|
||||||
|
method: "POST",
|
||||||
|
jsonData: {
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.SetAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auth information
|
||||||
|
*/
|
||||||
|
static async GetAuthInfo(): Promise<AuthInfo> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/auth/info",
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out
|
||||||
|
*/
|
||||||
|
static async SignOut(): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/auth/sign_out",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.UnsetAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
29
central_frontend/src/api/ServerApi.ts
Normal file
29
central_frontend/src/api/ServerApi.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface ServerConfig {
|
||||||
|
auth_disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ServerConfig | null = null;
|
||||||
|
|
||||||
|
export class ServerApi {
|
||||||
|
/**
|
||||||
|
* Get server configuration
|
||||||
|
*/
|
||||||
|
static async LoadConfig(): Promise<void> {
|
||||||
|
config = (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/server/config",
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached configuration
|
||||||
|
*/
|
||||||
|
static get Config(): ServerConfig {
|
||||||
|
if (config === null) throw new Error("Missing configuration!");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
@ -72,10 +72,10 @@ export function ConfirmDialogProvider(
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => handleClose(false)} autoFocus>
|
<Button onClick={() => handleClose(false)} autoFocus>
|
||||||
Annuler
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => handleClose(true)} color="error">
|
<Button onClick={() => handleClose(true)} color="error">
|
||||||
{confirmButton ?? "Confirmer"}
|
{confirmButton ?? "Confirm"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -11,6 +11,8 @@ import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider";
|
|||||||
import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider";
|
import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider";
|
||||||
import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider";
|
import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { ServerApi } from "./api/ServerApi";
|
||||||
|
import { AsyncWidget } from "./widgets/AsyncWidget";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@ -19,7 +21,12 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<ConfirmDialogProvider>
|
<ConfirmDialogProvider>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<LoadingMessageProvider>
|
<LoadingMessageProvider>
|
||||||
<App />
|
<AsyncWidget
|
||||||
|
loadKey={1}
|
||||||
|
load={async () => await ServerApi.LoadConfig()}
|
||||||
|
errMsg="Failed to connect to backend to retrieve static config!"
|
||||||
|
build={() => <App />}
|
||||||
|
/>
|
||||||
</LoadingMessageProvider>
|
</LoadingMessageProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</ConfirmDialogProvider>
|
</ConfirmDialogProvider>
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import * as React from "react";
|
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||||
|
import { Alert } from "@mui/material";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import TextField from "@mui/material/TextField";
|
import Grid from "@mui/material/Grid";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
||||||
import Checkbox from "@mui/material/Checkbox";
|
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import Box from "@mui/material/Box";
|
import TextField from "@mui/material/TextField";
|
||||||
import Grid from "@mui/material/Grid";
|
|
||||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { AuthApi } from "../api/AuthApi";
|
||||||
|
|
||||||
function Copyright(props: any) {
|
function Copyright(props: any) {
|
||||||
return (
|
return (
|
||||||
@ -31,12 +33,28 @@ function Copyright(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoginRoute() {
|
export function LoginRoute() {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
const [user, setUser] = React.useState("");
|
const [user, setUser] = React.useState("");
|
||||||
const [password, setPassword] = React.useState("");
|
const [password, setPassword] = React.useState("");
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const [error, setError] = React.useState<string | undefined>();
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// TODO
|
try {
|
||||||
|
loadingMessage.show("Signing in...");
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
await AuthApi.AuthWithPassword(user, password);
|
||||||
|
|
||||||
|
location.href = "/";
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to perform login!", e);
|
||||||
|
setError(`Failed to authenticate! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -73,6 +91,9 @@ export function LoginRoute() {
|
|||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
SolarEnergy
|
SolarEnergy
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{error && <Alert severity="error">{error}</Alert>}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
92
central_frontend/src/widgets/AsyncWidget.tsx
Normal file
92
central_frontend/src/widgets/AsyncWidget.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncWidget(p: {
|
||||||
|
loadKey: any;
|
||||||
|
load: () => Promise<void>;
|
||||||
|
errMsg: string;
|
||||||
|
build: () => React.ReactElement;
|
||||||
|
ready?: boolean;
|
||||||
|
errAdditionalElement?: () => React.ReactElement;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [state, setState] = useState(State.Loading);
|
||||||
|
|
||||||
|
const counter = useRef<any | null>(null);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setState(State.Loading);
|
||||||
|
await p.load();
|
||||||
|
setState(State.Ready);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setState(State.Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (counter.current === p.loadKey) return;
|
||||||
|
counter.current = p.loadKey;
|
||||||
|
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state === State.Error)
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
flex: "1",
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[900],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
variant="outlined"
|
||||||
|
severity="error"
|
||||||
|
style={{ margin: "0px 15px 15px 15px" }}
|
||||||
|
>
|
||||||
|
{p.errMsg}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Button onClick={load}>Try again</Button>
|
||||||
|
|
||||||
|
{p.errAdditionalElement && p.errAdditionalElement()}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state === State.Loading || p.ready === false)
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
flex: "1",
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[900],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return p.build();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user