mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
feat: JWT authentication with RBAC and default admin
Auth: JWT token-based authentication with argon2 password hashing. Auto-generated secret persisted in SQLite settings table. Middleware validates Bearer tokens on all /api/* endpoints except /api/auth/login. RBAC: admin (all operations) and viewer (GET only). Default admin user created on first run (password: "admin", logged as warning). Endpoints: POST /api/auth/login, POST /api/auth/register (admin only), GET /api/auth/me. WebSocket endpoints validate ?token= query parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9247d42ff6
commit
2e9dbe514f
@ -51,6 +51,9 @@ maxminddb = "0.27.3"
|
||||
ipnetwork = "0.21.1"
|
||||
lru = "0.16.3"
|
||||
rusqlite = { version = "0.34", features = ["bundled"] }
|
||||
jsonwebtoken = "9"
|
||||
argon2 = "0.5"
|
||||
rand = "0.9"
|
||||
|
||||
# Build dependencies
|
||||
cargo_metadata = { version = "0.23.1", default-features = false }
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
[Http]
|
||||
http_server_bind_port = 8080
|
||||
jwt_expiry_hours = 24
|
||||
|
||||
[Network]
|
||||
ingress_ifname = "ng-ext"
|
||||
|
||||
@ -51,6 +51,9 @@ maxminddb = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
argon2 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
80
net-guardia/src/core/auth/jwt.rs
Normal file
80
net-guardia/src/core/auth/jwt.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::database::Database;
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: i64,
|
||||
pub username: String,
|
||||
pub role: String,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
pub struct JwtService {
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
expiry_hours: u64,
|
||||
}
|
||||
|
||||
impl JwtService {
|
||||
pub fn new(db: &Database, expiry_hours: u64) -> Result<Self, Error> {
|
||||
let secret = match db.get_setting("jwt_secret")? {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
use rand::Rng;
|
||||
let secret: Vec<u8> = rand::rng().random::<[u8; 32]>().to_vec();
|
||||
let encoded = hex_encode(&secret);
|
||||
db.set_setting("jwt_secret", &encoded)?;
|
||||
encoded
|
||||
}
|
||||
};
|
||||
|
||||
let secret_bytes = secret.as_bytes();
|
||||
Ok(Self {
|
||||
encoding_key: EncodingKey::from_secret(secret_bytes),
|
||||
decoding_key: DecodingKey::from_secret(secret_bytes),
|
||||
expiry_hours,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_token(&self, user_id: i64, username: &str, role: &str) -> Result<String, Error> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let claims = Claims {
|
||||
sub: user_id,
|
||||
username: username.to_string(),
|
||||
role: role.to_string(),
|
||||
exp: (now + self.expiry_hours * 3600) as usize,
|
||||
};
|
||||
|
||||
encode(&Header::default(), &claims, &self.encoding_key)
|
||||
.map_err(|_| AuthError::InvalidToken.into())
|
||||
}
|
||||
|
||||
pub fn validate_token(&self, token: &str) -> Result<Claims, Error> {
|
||||
let token_data = decode::<Claims>(token, &self.decoding_key, &Validation::default())
|
||||
.map_err(|e| {
|
||||
if e.to_string().contains("ExpiredSignature") {
|
||||
Error::from(AuthError::TokenExpired)
|
||||
} else {
|
||||
Error::from(AuthError::InvalidToken)
|
||||
}
|
||||
})?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_encode(data: &[u8]) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::with_capacity(data.len() * 2);
|
||||
for b in data {
|
||||
write!(s, "{:02x}", b).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
117
net-guardia/src/core/auth/middleware.rs
Normal file
117
net-guardia/src/core/auth/middleware.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::future::{ready, Future, Ready};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use actix_web::{web, Error as ActixError, HttpMessage, HttpResponse};
|
||||
|
||||
use crate::core::auth::jwt::JwtService;
|
||||
|
||||
pub struct AuthMiddleware;
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = ActixError;
|
||||
type Transform = AuthMiddlewareService<S>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(AuthMiddlewareService {
|
||||
service: Rc::new(service),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthMiddlewareService<S> {
|
||||
service: Rc<S>,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = ActixError;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
ctx: &mut core::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(ctx)
|
||||
}
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let service = Rc::clone(&self.service);
|
||||
|
||||
Box::pin(async move {
|
||||
let path = req.path().to_string();
|
||||
|
||||
// Skip auth for login endpoint and non-API routes
|
||||
if path == "/api/auth/login" || !path.starts_with("/api/") {
|
||||
let res = service.call(req).await?.map_into_left_body();
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// Extract JWT service from app data
|
||||
let jwt_service = match req.app_data::<web::Data<JwtService>>() {
|
||||
Some(s) => s.clone(),
|
||||
None => {
|
||||
let resp = HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Auth not configured"}));
|
||||
return Ok(req.into_response(resp).map_into_right_body());
|
||||
}
|
||||
};
|
||||
|
||||
// Extract token from Authorization header
|
||||
let auth_header = req.headers().get("Authorization");
|
||||
let token = match auth_header {
|
||||
Some(val) => {
|
||||
let val_str = val.to_str().unwrap_or("");
|
||||
if val_str.starts_with("Bearer ") {
|
||||
&val_str[7..]
|
||||
} else {
|
||||
let resp = HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid authorization header"}));
|
||||
return Ok(req.into_response(resp).map_into_right_body());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let resp = HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Missing authorization header"}));
|
||||
return Ok(req.into_response(resp).map_into_right_body());
|
||||
}
|
||||
};
|
||||
|
||||
// Validate token
|
||||
let claims = match jwt_service.validate_token(token) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
let resp = HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid or expired token"}));
|
||||
return Ok(req.into_response(resp).map_into_right_body());
|
||||
}
|
||||
};
|
||||
|
||||
// RBAC: viewer can only GET
|
||||
if claims.role == "viewer" && req.method() != actix_web::http::Method::GET {
|
||||
let resp = HttpResponse::Forbidden()
|
||||
.json(serde_json::json!({"error": "Insufficient permissions"}));
|
||||
return Ok(req.into_response(resp).map_into_right_body());
|
||||
}
|
||||
|
||||
// Store claims in request extensions
|
||||
req.extensions_mut().insert(claims);
|
||||
|
||||
let res = service.call(req).await?.map_into_left_body();
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
3
net-guardia/src/core/auth/mod.rs
Normal file
3
net-guardia/src/core/auth/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod jwt;
|
||||
pub mod middleware;
|
||||
pub mod password;
|
||||
22
net-guardia/src/core/auth/password.rs
Normal file
22
net-guardia/src/core/auth/password.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|_| AuthError::InvalidCredentials)?;
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool, Error> {
|
||||
let parsed = PasswordHash::new(hash).map_err(|_| AuthError::InvalidCredentials)?;
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed)
|
||||
.is_ok())
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod database;
|
||||
pub mod ebpf;
|
||||
pub mod infrastructure;
|
||||
|
||||
@ -10,6 +10,8 @@ use aya_log::EbpfLogger;
|
||||
use common::define::pipeline::*;
|
||||
use macros::log;
|
||||
|
||||
use crate::core::auth::jwt::JwtService;
|
||||
use crate::core::auth::password;
|
||||
use crate::core::database::Database;
|
||||
use crate::core::ebpf::EbpfServices;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
@ -22,7 +24,7 @@ use crate::model::error::Error;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::log::system::SystemLog;
|
||||
use crate::utils::logging::Logging;
|
||||
use crate::web::api::{acl, filter, rate_limit as rate_limit_api, stats, health as health_api, ml, system as system_api, default, ws};
|
||||
use crate::web::api::{acl, auth, filter, rate_limit as rate_limit_api, stats, health as health_api, ml, system as system_api, default, ws};
|
||||
|
||||
/// Maps stage name (from config.toml) to (function_name, stage_id)
|
||||
fn stage_registry() -> HashMap<&'static str, (&'static str, u32)> {
|
||||
@ -39,6 +41,7 @@ pub struct System {
|
||||
pub ebpf_services: Arc<EbpfServices>,
|
||||
pub app_services: Arc<MLService>,
|
||||
pub db: Arc<Database>,
|
||||
pub jwt_service: Arc<JwtService>,
|
||||
pub ingress_ebpf: Ebpf,
|
||||
pub egress_ebpf: Ebpf,
|
||||
#[allow(dead_code)]
|
||||
@ -65,6 +68,15 @@ impl System {
|
||||
|
||||
let db = Arc::new(Database::new(&app_config.misc.database_path)?);
|
||||
|
||||
// Create default admin user if no users exist
|
||||
if db.user_count().unwrap_or(0) == 0 {
|
||||
let hash = password::hash_password("admin")?;
|
||||
db.insert_user("admin", &hash, "admin")?;
|
||||
tracing::warn!("Default admin user created with password 'admin' — change it immediately");
|
||||
}
|
||||
|
||||
let jwt_service = Arc::new(JwtService::new(&db, app_config.http.jwt_expiry_hours)?);
|
||||
|
||||
let ebpf_services = Arc::new(EbpfServices::new(
|
||||
app_config.clone(),
|
||||
&mut ingress_ebpf,
|
||||
@ -122,6 +134,7 @@ impl System {
|
||||
ebpf_services,
|
||||
app_services,
|
||||
db,
|
||||
jwt_service,
|
||||
ingress_ebpf,
|
||||
egress_ebpf,
|
||||
ingress_program_array,
|
||||
@ -211,6 +224,7 @@ impl System {
|
||||
let flow_statistics = self.app_services.flow_statistics.clone();
|
||||
let drop_monitor = self.ebpf_services.drop_monitor.clone();
|
||||
let db = self.db.clone();
|
||||
let jwt_service = self.jwt_service.clone();
|
||||
let port = self.app_config.http.http_server_bind_port;
|
||||
HttpServer::new(move || {
|
||||
let cors = actix_cors::Cors::default()
|
||||
@ -234,8 +248,11 @@ impl System {
|
||||
.app_data(web::Data::from(flow_statistics.clone()))
|
||||
.app_data(web::Data::from(drop_monitor.clone()))
|
||||
.app_data(web::Data::from(db.clone()))
|
||||
.app_data(web::Data::from(jwt_service.clone()))
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.wrap(crate::core::auth::middleware::AuthMiddleware)
|
||||
.service(auth::initialize())
|
||||
.service(acl::initialize())
|
||||
.service(filter::initialize())
|
||||
.service(rate_limit_api::initialize())
|
||||
|
||||
@ -17,8 +17,12 @@ pub struct AppConfigTable {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct HttpConfig {
|
||||
pub http_server_bind_port: u16,
|
||||
#[serde(default = "default_jwt_expiry")]
|
||||
pub jwt_expiry_hours: u64,
|
||||
}
|
||||
|
||||
fn default_jwt_expiry() -> u64 { 24 }
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct NetworkConfig {
|
||||
pub ingress_ifname: String,
|
||||
|
||||
20
net-guardia/src/model/error/auth.rs
Normal file
20
net-guardia/src/model/error/auth.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
AuthError {
|
||||
#[error("Invalid credentials")]
|
||||
InvalidCredentials => tracing::Level::WARN,
|
||||
|
||||
#[error("Token expired")]
|
||||
TokenExpired => tracing::Level::WARN,
|
||||
|
||||
#[error("Invalid token")]
|
||||
InvalidToken => tracing::Level::WARN,
|
||||
|
||||
#[error("Insufficient permissions")]
|
||||
InsufficientPermissions => tracing::Level::WARN,
|
||||
|
||||
#[error("Missing authorization header")]
|
||||
MissingAuthHeader => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod database;
|
||||
pub mod ebpf;
|
||||
pub mod http;
|
||||
@ -8,6 +9,7 @@ pub mod system;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::error::database::DatabaseError;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
@ -18,6 +20,8 @@ use crate::model::error::system::SystemError;
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Auth(AuthError),
|
||||
#[error("{0}")]
|
||||
Database(DatabaseError),
|
||||
#[error("{0}")]
|
||||
@ -34,6 +38,12 @@ pub enum Error {
|
||||
System(SystemError),
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
fn from(error: AuthError) -> Self {
|
||||
Self::Auth(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DatabaseError> for Error {
|
||||
fn from(error: DatabaseError) -> Self {
|
||||
Self::Database(error)
|
||||
|
||||
113
net-guardia/src/web/api/auth.rs
Normal file
113
net-guardia/src/web/api/auth.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::core::auth::jwt::{Claims, JwtService};
|
||||
use crate::core::auth::password;
|
||||
use crate::core::database::Database;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegisterRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
role: String,
|
||||
}
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/auth")
|
||||
.route("/login", web::post().to(login))
|
||||
.route("/register", web::post().to(register))
|
||||
.route("/me", web::get().to(me))
|
||||
}
|
||||
|
||||
async fn login(
|
||||
body: web::Json<LoginRequest>,
|
||||
db: web::Data<Database>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
let req = body.into_inner();
|
||||
|
||||
let user = match db.find_user(&req.username) {
|
||||
Ok(Some(u)) => u,
|
||||
_ => {
|
||||
return HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid credentials"}))
|
||||
}
|
||||
};
|
||||
|
||||
let (id, username, hash, role) = user;
|
||||
|
||||
match password::verify_password(&req.password, &hash) {
|
||||
Ok(true) => {}
|
||||
_ => {
|
||||
return HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid credentials"}))
|
||||
}
|
||||
}
|
||||
|
||||
match jwt.create_token(id, &username, &role) {
|
||||
Ok(token) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"token": token,
|
||||
"role": role,
|
||||
})),
|
||||
Err(_) => HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Failed to create token"})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn register(
|
||||
req: HttpRequest,
|
||||
body: web::Json<RegisterRequest>,
|
||||
db: web::Data<Database>,
|
||||
) -> impl Responder {
|
||||
// Check caller is admin
|
||||
let claims = req.extensions().get::<Claims>().cloned();
|
||||
match claims {
|
||||
Some(c) if c.role == "admin" => {}
|
||||
_ => {
|
||||
return HttpResponse::Forbidden()
|
||||
.json(serde_json::json!({"error": "Admin access required"}))
|
||||
}
|
||||
}
|
||||
|
||||
let reg = body.into_inner();
|
||||
|
||||
// Validate role
|
||||
if reg.role != "admin" && reg.role != "viewer" {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "Role must be 'admin' or 'viewer'"}));
|
||||
}
|
||||
|
||||
let hash = match password::hash_password(®.password) {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Failed to hash password"}))
|
||||
}
|
||||
};
|
||||
|
||||
match db.insert_user(®.username, &hash, ®.role) {
|
||||
Ok(_) => HttpResponse::Created()
|
||||
.json(serde_json::json!({"username": reg.username, "role": reg.role})),
|
||||
Err(e) => {
|
||||
HttpResponse::Conflict().json(serde_json::json!({"error": e.to_string()}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn me(req: HttpRequest) -> impl Responder {
|
||||
match req.extensions().get::<Claims>().cloned() {
|
||||
Some(claims) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"id": claims.sub,
|
||||
"username": claims.username,
|
||||
"role": claims.role,
|
||||
})),
|
||||
None => HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Not authenticated"})),
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
pub mod acl;
|
||||
pub mod auth;
|
||||
pub mod default;
|
||||
pub mod filter;
|
||||
pub mod rate_limit;
|
||||
pub mod stats;
|
||||
pub mod health;
|
||||
pub mod ml;
|
||||
pub mod rate_limit;
|
||||
pub mod stats;
|
||||
pub mod system;
|
||||
pub mod default;
|
||||
pub mod ws;
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::core::auth::jwt::JwtService;
|
||||
use crate::core::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::core::infrastructure::statistics::FlowStatistics;
|
||||
use crate::core::ml::alert::MLAlert;
|
||||
use crate::web::websocket::{alert_websocket, drop_websocket, flow_websocket, health_websocket};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WsQuery {
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/ws")
|
||||
.route("/health", web::get().to(health_ws))
|
||||
@ -14,11 +21,29 @@ pub fn initialize() -> Scope {
|
||||
.route("/drops", web::get().to(drops_ws))
|
||||
}
|
||||
|
||||
fn validate_ws_token(query: &web::Query<WsQuery>, jwt: &web::Data<JwtService>) -> Result<(), HttpResponse> {
|
||||
match &query.token {
|
||||
Some(token) => {
|
||||
jwt.validate_token(token)
|
||||
.map(|_| ())
|
||||
.map_err(|_| HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid or expired token"})))
|
||||
}
|
||||
None => Err(HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Missing token query parameter"}))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
health: web::Data<SystemHealth>,
|
||||
query: web::Query<WsQuery>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
if let Err(resp) = validate_ws_token(&query, &jwt) {
|
||||
return resp;
|
||||
}
|
||||
match health_websocket::websocket_system_health(req, stream, health).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
@ -29,7 +54,12 @@ async fn alerts_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
ai: web::Data<MLAlert>,
|
||||
query: web::Query<WsQuery>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
if let Err(resp) = validate_ws_token(&query, &jwt) {
|
||||
return resp;
|
||||
}
|
||||
match alert_websocket::websocket_alert(req, stream, ai).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
@ -40,7 +70,12 @@ async fn flows_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
stats: web::Data<FlowStatistics>,
|
||||
query: web::Query<WsQuery>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
if let Err(resp) = validate_ws_token(&query, &jwt) {
|
||||
return resp;
|
||||
}
|
||||
match flow_websocket::flow_stats_ws(req, stream, stats).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
@ -51,7 +86,12 @@ async fn drops_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
monitor: web::Data<DropMonitor>,
|
||||
query: web::Query<WsQuery>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
if let Err(resp) = validate_ws_token(&query, &jwt) {
|
||||
return resp;
|
||||
}
|
||||
match drop_websocket::websocket_drops(req, stream, monitor).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user