Expand parser, fixed comments
This commit is contained in:
4
.env
4
.env
@@ -18,10 +18,10 @@ MINIO_PORT_INTERNAL=9001
|
|||||||
|
|
||||||
API_HOST=localhost
|
API_HOST=localhost
|
||||||
API_PORT=5000
|
API_PORT=5000
|
||||||
ADMIN_USERNAME=admin
|
ENVIRONMENT=development
|
||||||
|
ADMIN_EMAIL=admin@example.com
|
||||||
ADMIN_PASSWORD=CHANGEME
|
ADMIN_PASSWORD=CHANGEME
|
||||||
|
|
||||||
UI_PORT=3000
|
UI_PORT=3000
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
post, web, HttpResponse, ResponseError,
|
post, web, HttpResponse, ResponseError,
|
||||||
cookie::{Cookie, time::Duration},
|
cookie::{Cookie, time::Duration},
|
||||||
@@ -12,23 +13,36 @@ use crate::{
|
|||||||
use crate::auth::{Auth, DEFAULT_SESSION_TTL};
|
use crate::auth::{Auth, DEFAULT_SESSION_TTL};
|
||||||
|
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(user: web::Json<RegisterRequest>) -> HttpResponse {
|
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||||
let register_user = user.0;
|
let register_user = user.into_inner();
|
||||||
let insert_user: User = match register_user.to_user() {
|
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,
|
Ok(user) => user,
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(&err),
|
||||||
};
|
};
|
||||||
|
|
||||||
match insert_user.insert().await {
|
match insert_user.insert().await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let response: UserResponse = user.into();
|
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)
|
HttpResponse::Created().json(response)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Obfuscate the service error message to prevent leaking database details
|
// Obfuscate the service error message to prevent leaking database details
|
||||||
if err.status == 409 {
|
if err.status == 409 {
|
||||||
|
log::warn!(
|
||||||
|
"Duplicate user registration attempt [Email: {}] [IP Address: {}]",
|
||||||
|
email,
|
||||||
|
ip_address
|
||||||
|
);
|
||||||
HttpResponse::Conflict().finish()
|
HttpResponse::Conflict().finish()
|
||||||
} else {
|
} else {
|
||||||
|
log::error!("attemptFailed to register user [Email: {}]: {}", email, err);
|
||||||
ResponseError::error_response(&err)
|
ResponseError::error_response(&err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,28 +65,54 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
|||||||
let session_cookie = session.to_cookie();
|
let session_cookie = session.to_cookie();
|
||||||
// Save the session to the database
|
// Save the session to the database
|
||||||
if let Err(err) = session.store().await {
|
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()));
|
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()
|
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||||
} else {
|
} else {
|
||||||
log::error!("Invalid login attempt for {}", email);
|
log::error!(
|
||||||
|
"Invalid login attempt [Email: {}] [IP Address: {}]",
|
||||||
|
email,
|
||||||
|
ip_address
|
||||||
|
);
|
||||||
HttpResponse::Unauthorized().finish()
|
HttpResponse::Unauthorized().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/logout")]
|
#[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
|
// Delete the session from the store
|
||||||
match req.cookie(SESSION_COOKIE_NAME) {
|
match req.cookie(SESSION_COOKIE_NAME) {
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
let session_id = cookie.value().to_string();
|
let session_id = cookie.value().to_string();
|
||||||
if let Err(err) = Session::delete(&session_id).await {
|
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()));
|
return ResponseError::error_response(&Error::new(500, err.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
log::error!(
|
||||||
|
"Invalid logout attempt [Email: {}] [IP Address: {}]",
|
||||||
|
email,
|
||||||
|
ip_address
|
||||||
|
);
|
||||||
return ResponseError::error_response(&Error::new(400, "Invalid session".to_string()));
|
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)
|
.http_only(true)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Successful logout attempt [Email: {}] [IP Address: {}]",
|
||||||
|
email,
|
||||||
|
ip_address
|
||||||
|
);
|
||||||
HttpResponse::Ok().cookie(session_cookie).finish()
|
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,12 +93,26 @@ impl Session {
|
|||||||
None => DEFAULT_SESSION_TTL,
|
None => DEFAULT_SESSION_TTL,
|
||||||
};
|
};
|
||||||
let ttl = expires_at - Utc::now().timestamp();
|
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("/")
|
.path("/")
|
||||||
.max_age(Duration::seconds(ttl))
|
.max_age(Duration::seconds(ttl))
|
||||||
// TODO: enable secure and http_only
|
// TODO: enable secure and http_only
|
||||||
// .secure(true)
|
.secure(true)
|
||||||
// .http_only(true)
|
.http_only(true)
|
||||||
.finish()
|
.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let port = env::var("API_PORT").unwrap_or("5000".to_string());
|
let port = env::var("API_PORT").unwrap_or("5000".to_string());
|
||||||
|
|
||||||
// Initialize admin user
|
// Initialize admin user
|
||||||
let admin_username = env::var("ADMIN_USERNAME");
|
let admin_email = env::var("ADMIN_EMAIL");
|
||||||
let admin_password = env::var("ADMIN_PASSWORD");
|
let admin_password = env::var("ADMIN_PASSWORD");
|
||||||
if admin_username.is_ok() && admin_password.is_ok() {
|
if admin_email.is_ok() && admin_password.is_ok() {
|
||||||
let username = admin_username.unwrap();
|
let email = admin_email.unwrap();
|
||||||
if User::select(&username).await.is_none() {
|
if User::select(&email).await.is_none() {
|
||||||
log::debug!("Creating default administrator");
|
log::debug!("Creating default administrator");
|
||||||
let password = admin_password.unwrap();
|
let password = admin_password.unwrap();
|
||||||
let password_hash = hash(&password)?;
|
let password_hash = hash(&password)?;
|
||||||
let admin_user = User {
|
let admin_user = User {
|
||||||
email: username,
|
email,
|
||||||
password_hash,
|
password_hash,
|
||||||
role: ADMIN_ROLE.to_string(),
|
role: ADMIN_ROLE.to_string(),
|
||||||
first_name: "Admin".to_string(),
|
first_name: "Admin".to_string(),
|
||||||
|
|||||||
@@ -82,6 +82,20 @@ pub struct QualityControlFlags {
|
|||||||
pub no_significant_change: Option<bool>,
|
pub no_significant_change: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub temporary_change: Option<bool>,
|
pub temporary_change: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rvr_missing: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub precipication_identifier_information_not_available: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub precipication_information_not_available: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub freezing_rain_information_not_available: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub thunderstorm_information_not_available: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub visibility_at_secondary_location_not_available: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sky_condition_at_secondary_location_not_available: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QualityControlFlags {
|
impl Default for QualityControlFlags {
|
||||||
@@ -94,6 +108,13 @@ impl Default for QualityControlFlags {
|
|||||||
corrected: None,
|
corrected: None,
|
||||||
no_significant_change: None,
|
no_significant_change: None,
|
||||||
temporary_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
|
// Station Identifier
|
||||||
metar.station_id = metar_parts[0].to_string();
|
metar.station_id = metar_parts[0].to_string();
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
@@ -602,6 +628,36 @@ impl Metar {
|
|||||||
metar.quality_control_flags.auto_station_with_precipication = Some(true);
|
metar.quality_control_flags.auto_station_with_precipication = Some(true);
|
||||||
} else if remark == "$" {
|
} else if remark == "$" {
|
||||||
metar.quality_control_flags.maintenance_indicator_on = Some(true);
|
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) {
|
} else if slp_re.is_match(remark) {
|
||||||
let slp = slp_re.captures(remark).unwrap();
|
let slp = slp_re.captures(remark).unwrap();
|
||||||
let sea_level_pressure = slp[1].parse::<f64>().unwrap();
|
let sea_level_pressure = slp[1].parse::<f64>().unwrap();
|
||||||
@@ -853,3 +909,18 @@ impl Metar {
|
|||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ impl User {
|
|||||||
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<User> {
|
pub async fn insert(&self) -> ApiResult<User> {
|
||||||
let pool = db::pool();
|
let pool = db::pool();
|
||||||
let user: User = sqlx::query_as::<_, Self>(&format!(
|
let user: User = sqlx::query_as::<_, Self>(&format!(
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{BASE_URL}}/airports?icaos=00AA&page=1&limit=1000
|
url: {{BASE_URL}}/airports?page=1&limit=1000
|
||||||
body: none
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
params:query {
|
params:query {
|
||||||
icaos: 00AA
|
|
||||||
page: 1
|
page: 1
|
||||||
limit: 1000
|
limit: 1000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
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
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
params:query {
|
params:query {
|
||||||
icaos: KHEF,KJYO,KLNS,KRMN,KIAD,KSFO,KPBI,KJFK,KORD,KDAL,KSAN,KGFK
|
icaos: KJYO,KOKV,KMRB,KHEF,KIAD
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ post {
|
|||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"email": "admin",
|
"email": "admin@example.com",
|
||||||
"password": "CHANGEME"
|
"password": "CHANGEME"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ services:
|
|||||||
- redis:/data
|
- redis:/data
|
||||||
ports:
|
ports:
|
||||||
- ${REDIS_PORT:-6379}:6379
|
- ${REDIS_PORT:-6379}:6379
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
profiles:
|
profiles:
|
||||||
|
|||||||
Reference in New Issue
Block a user