From ce6d02be345c2295c27bfe96f93e895bf077b1c2 Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Mon, 7 Apr 2025 22:58:52 -0400 Subject: [PATCH] Expand parser, fixed comments --- .env | 4 +- api/src/auth/routes.rs | 61 +++++++++++++++++++++---- api/src/auth/session.rs | 22 +++++++-- api/src/main.rs | 10 ++-- api/src/metars/model.rs | 71 +++++++++++++++++++++++++++++ api/src/users/model.rs | 14 ++++++ bruno/Airports/Get All Airports.bru | 3 +- bruno/Metars/Find Metars.bru | 4 +- bruno/Users/Login.bru | 2 +- docker-compose.yml | 5 ++ 10 files changed, 172 insertions(+), 24 deletions(-) diff --git a/.env b/.env index b244b62..5af770b 100644 --- a/.env +++ b/.env @@ -18,10 +18,10 @@ MINIO_PORT_INTERNAL=9001 API_HOST=localhost API_PORT=5000 -ADMIN_USERNAME=admin +ENVIRONMENT=development +ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=CHANGEME UI_PORT=3000 -NODE_ENV=development GOV_API_URL=https://aviationweather.gov/cgi-bin/data diff --git a/api/src/auth/routes.rs b/api/src/auth/routes.rs index e40544f..3ca5d8d 100644 --- a/api/src/auth/routes.rs +++ b/api/src/auth/routes.rs @@ -1,3 +1,4 @@ +use std::sync::OnceLock; use actix_web::{ post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, @@ -12,23 +13,36 @@ use crate::{ use crate::auth::{Auth, DEFAULT_SESSION_TTL}; #[post("/register")] -async fn register(user: web::Json) -> HttpResponse { - let register_user = user.0; - let insert_user: User = match register_user.to_user() { +async fn register(user: web::Json, req: HttpRequest) -> HttpResponse { + let register_user = user.into_inner(); + let email = register_user.email.clone(); + let ip_address = req.peer_addr().unwrap().ip().to_string(); + let mut insert_user: User = match register_user.to_user() { Ok(user) => user, Err(err) => return ResponseError::error_response(&err), }; + match insert_user.insert().await { Ok(user) => { let response: UserResponse = user.into(); - log::trace!("Registered user '{}'", response.email); + log::info!( + "Successful user registration [Email: {}] [IP Address: {}]", + email, + ip_address + ); HttpResponse::Created().json(response) } Err(err) => { // Obfuscate the service error message to prevent leaking database details if err.status == 409 { + log::warn!( + "Duplicate user registration attempt [Email: {}] [IP Address: {}]", + email, + ip_address + ); HttpResponse::Conflict().finish() } else { + log::error!("attemptFailed to register user [Email: {}]: {}", email, err); ResponseError::error_response(&err) } } @@ -51,28 +65,54 @@ async fn login(request: web::Json, req: HttpRequest) -> HttpRespon let session_cookie = session.to_cookie(); // Save the session to the database if let Err(err) = session.store().await { - log::error!("Failed to store session"); + log::error!( + "Login attempt failure [Email: {}] [IP Address: {}]: {}", + email, + ip_address, + err + ); return ResponseError::error_response(&Error::new(500, err.to_string())); } + log::info!( + "Successful login attempt [Email: {}] [IP Address: {}]", + email, + ip_address + ); HttpResponse::Ok().cookie(session_cookie).finish() } else { - log::error!("Invalid login attempt for {}", email); + log::error!( + "Invalid login attempt [Email: {}] [IP Address: {}]", + email, + ip_address + ); HttpResponse::Unauthorized().finish() } } #[post("/logout")] -async fn logout(req: HttpRequest, _auth: Auth) -> HttpResponse { +async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { + let email = auth.user.email; + let ip_address = req.peer_addr().unwrap().ip().to_string(); // Delete the session from the store match req.cookie(SESSION_COOKIE_NAME) { Some(cookie) => { let session_id = cookie.value().to_string(); if let Err(err) = Session::delete(&session_id).await { - log::error!("Failed to delete session"); + log::error!( + "Logout attempt failure [Email: {}] [IP Address: {}]: {}", + email, + ip_address, + err + ); return ResponseError::error_response(&Error::new(500, err.to_string())); } } None => { + log::error!( + "Invalid logout attempt [Email: {}] [IP Address: {}]", + email, + ip_address + ); return ResponseError::error_response(&Error::new(400, "Invalid session".to_string())); } } @@ -84,6 +124,11 @@ async fn logout(req: HttpRequest, _auth: Auth) -> HttpResponse { .http_only(true) .finish(); + log::info!( + "Successful logout attempt [Email: {}] [IP Address: {}]", + email, + ip_address + ); HttpResponse::Ok().cookie(session_cookie).finish() } diff --git a/api/src/auth/session.rs b/api/src/auth/session.rs index 33e3cb4..abe1aa9 100644 --- a/api/src/auth/session.rs +++ b/api/src/auth/session.rs @@ -93,12 +93,26 @@ impl Session { None => DEFAULT_SESSION_TTL, }; let ttl = expires_at - Utc::now().timestamp(); - Cookie::build(SESSION_COOKIE_NAME, self.session_id.clone()) + let mut cookie = Cookie::build(SESSION_COOKIE_NAME, self.session_id.clone()) .path("/") .max_age(Duration::seconds(ttl)) // TODO: enable secure and http_only - // .secure(true) - // .http_only(true) - .finish() + .secure(true) + .http_only(true) + .finish(); + + if let Ok(environment) = std::env::var("ENVIRONMENT") { + if environment == "development" || environment == "dev" { + log::debug!( + "Development cookie [Email: {}]: {}", + self.email, + self.session_id + ); + cookie.set_secure(false); + cookie.set_http_only(false); + } + } + + cookie } } diff --git a/api/src/main.rs b/api/src/main.rs index d536c64..8f1aea6 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -24,16 +24,16 @@ async fn main() -> Result<(), Box> { let port = env::var("API_PORT").unwrap_or("5000".to_string()); // Initialize admin user - let admin_username = env::var("ADMIN_USERNAME"); + let admin_email = env::var("ADMIN_EMAIL"); let admin_password = env::var("ADMIN_PASSWORD"); - if admin_username.is_ok() && admin_password.is_ok() { - let username = admin_username.unwrap(); - if User::select(&username).await.is_none() { + if admin_email.is_ok() && admin_password.is_ok() { + let email = admin_email.unwrap(); + if User::select(&email).await.is_none() { log::debug!("Creating default administrator"); let password = admin_password.unwrap(); let password_hash = hash(&password)?; let admin_user = User { - email: username, + email, password_hash, role: ADMIN_ROLE.to_string(), first_name: "Admin".to_string(), diff --git a/api/src/metars/model.rs b/api/src/metars/model.rs index d231e1f..6a985b6 100644 --- a/api/src/metars/model.rs +++ b/api/src/metars/model.rs @@ -82,6 +82,20 @@ pub struct QualityControlFlags { pub no_significant_change: Option, #[serde(skip_serializing_if = "Option::is_none")] pub temporary_change: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rvr_missing: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub precipication_identifier_information_not_available: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub precipication_information_not_available: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub freezing_rain_information_not_available: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thunderstorm_information_not_available: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility_at_secondary_location_not_available: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sky_condition_at_secondary_location_not_available: Option, } impl Default for QualityControlFlags { @@ -94,6 +108,13 @@ impl Default for QualityControlFlags { corrected: None, no_significant_change: None, temporary_change: None, + rvr_missing: None, + precipication_identifier_information_not_available: None, + precipication_information_not_available: None, + freezing_rain_information_not_available: None, + thunderstorm_information_not_available: None, + visibility_at_secondary_location_not_available: None, + sky_condition_at_secondary_location_not_available: None, } } } @@ -202,6 +223,11 @@ impl Metar { )); } + // Remove METAR at start of text + if metar_parts[0].to_string() == "METAR".to_string() { + metar_parts.remove(0); + } + // Station Identifier metar.station_id = metar_parts[0].to_string(); metar_parts.remove(0); @@ -602,6 +628,36 @@ impl Metar { metar.quality_control_flags.auto_station_with_precipication = Some(true); } else if remark == "$" { metar.quality_control_flags.maintenance_indicator_on = Some(true); + } else if remark == "PNO" { + metar + .quality_control_flags + .precipication_information_not_available = Some(true); + } else if remark == "RVRNO" { + metar.quality_control_flags.rvr_missing = Some(true); + } else if remark == "PWINO" { + metar + .quality_control_flags + .precipication_identifier_information_not_available = Some(true); + } else if remark == "FZRANO" { + metar + .quality_control_flags + .freezing_rain_information_not_available = Some(true); + } else if remark == "TSNO" { + metar + .quality_control_flags + .thunderstorm_information_not_available = Some(true); + } else if remark == "VISNO" { + let location = metar_parts[0]; + metar_parts.remove(0); + metar + .quality_control_flags + .visibility_at_secondary_location_not_available = Some(location.to_string()); + } else if remark == "CHINO" { + let location = metar_parts[0]; + metar_parts.remove(0); + metar + .quality_control_flags + .sky_condition_at_secondary_location_not_available = Some(location.to_string()); } else if slp_re.is_match(remark) { let slp = slp_re.captures(remark).unwrap(); let sea_level_pressure = slp[1].parse::().unwrap(); @@ -853,3 +909,18 @@ impl Metar { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metar() { + let metar_string = "METAR KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT -RA BR BKN015 OVC025 06/04 A2990 +RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 RWY11 RAB07 CIG 013V017 CIG 017 RWY11 PRESFR +SLP125 P0003 60009 T00640036 10066 21012 58033 TSNO $".to_string(); + + let metar = Metar::parse(&metar_string).unwrap(); + dbg!(&metar); + } +} diff --git a/api/src/users/model.rs b/api/src/users/model.rs index c94edc1..c9d5e4a 100644 --- a/api/src/users/model.rs +++ b/api/src/users/model.rs @@ -96,6 +96,20 @@ impl User { user } + pub async fn count() -> i64 { + let pool = db::pool(); + + sqlx::query_scalar(&format!( + r#" + SELECT COUNT(*) FROM {} + "#, + TABLE_NAME + )) + .fetch_one(pool) + .await + .unwrap_or_else(|_| 0) + } + pub async fn insert(&self) -> ApiResult { let pool = db::pool(); let user: User = sqlx::query_as::<_, Self>(&format!( diff --git a/bruno/Airports/Get All Airports.bru b/bruno/Airports/Get All Airports.bru index 32fe234..5bfcc6e 100644 --- a/bruno/Airports/Get All Airports.bru +++ b/bruno/Airports/Get All Airports.bru @@ -5,13 +5,12 @@ meta { } get { - url: {{BASE_URL}}/airports?icaos=00AA&page=1&limit=1000 + url: {{BASE_URL}}/airports?page=1&limit=1000 body: none auth: none } params:query { - icaos: 00AA page: 1 limit: 1000 } diff --git a/bruno/Metars/Find Metars.bru b/bruno/Metars/Find Metars.bru index a7ef918..d388e22 100644 --- a/bruno/Metars/Find Metars.bru +++ b/bruno/Metars/Find Metars.bru @@ -5,11 +5,11 @@ meta { } get { - url: {{BASE_URL}}/metars?icaos=KHEF,KJYO,KLNS,KRMN,KIAD,KSFO,KPBI,KJFK,KORD,KDAL,KSAN,KGFK + url: {{BASE_URL}}/metars?icaos=KJYO,KOKV,KMRB,KHEF,KIAD body: none auth: none } params:query { - icaos: KHEF,KJYO,KLNS,KRMN,KIAD,KSFO,KPBI,KJFK,KORD,KDAL,KSAN,KGFK + icaos: KJYO,KOKV,KMRB,KHEF,KIAD } diff --git a/bruno/Users/Login.bru b/bruno/Users/Login.bru index 4069f89..3909e92 100644 --- a/bruno/Users/Login.bru +++ b/bruno/Users/Login.bru @@ -12,7 +12,7 @@ post { body:json { { - "email": "admin", + "email": "admin@example.com", "password": "CHANGEME" } } diff --git a/docker-compose.yml b/docker-compose.yml index 0d99e4e..4cc0178 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,11 @@ services: - redis:/data ports: - ${REDIS_PORT:-6379}:6379 + healthcheck: + test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] + interval: 10s + timeout: 5s + retries: 3 networks: - backend profiles: