Added airport data to map
This commit is contained in:
@@ -46,11 +46,12 @@ pub struct AirportQuery {
|
||||
pub icaos: Option<String>,
|
||||
pub iatas: Option<String>,
|
||||
pub locals: Option<String>,
|
||||
pub names: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub categories: Option<String>,
|
||||
pub iso_countries: Option<String>,
|
||||
pub iso_regions: Option<String>,
|
||||
pub municipalities: Option<String>,
|
||||
pub bounds: Option<String>,
|
||||
pub metars: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -62,16 +63,48 @@ impl Default for AirportQuery {
|
||||
icaos: None,
|
||||
iatas: None,
|
||||
locals: None,
|
||||
names: None,
|
||||
name: None,
|
||||
categories: None,
|
||||
iso_countries: None,
|
||||
iso_regions: None,
|
||||
municipalities: None,
|
||||
bounds: None,
|
||||
metars: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Bounds {
|
||||
pub north_east_lat: f32,
|
||||
pub north_east_lon: f32,
|
||||
pub south_west_lat: f32,
|
||||
pub south_west_lon: f32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn parse(input: &str) -> ApiResult<Bounds> {
|
||||
let parts: Vec<&str> = input.split(',').collect();
|
||||
if parts.len() != 4 {
|
||||
return Err(Error::new(
|
||||
400,
|
||||
format!("Expected 4 fields in bounds but received {}", parts.len()),
|
||||
));
|
||||
}
|
||||
let north_east_lat = parts[0].trim().parse::<f32>()?;
|
||||
let north_east_lon = parts[1].trim().parse::<f32>()?;
|
||||
let south_west_lat = parts[2].trim().parse::<f32>()?;
|
||||
let south_west_lon = parts[3].trim().parse::<f32>()?;
|
||||
|
||||
Ok(Bounds {
|
||||
north_east_lat,
|
||||
north_east_lon,
|
||||
south_west_lat,
|
||||
south_west_lon,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow)]
|
||||
struct AirportRow {
|
||||
pub icao: String,
|
||||
@@ -265,8 +298,20 @@ impl Airport {
|
||||
&query.municipalities,
|
||||
);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "local", &query.locals);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "name", &query.names);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "category", &query.categories);
|
||||
Self::push_condition_like(&mut builder, &mut has_where, "name", &query.name);
|
||||
Self::push_condition_bounds(&mut builder, &mut has_where, &query.bounds)?;
|
||||
|
||||
// Order by AircraftCategory
|
||||
builder.push(" ORDER BY CASE category ");
|
||||
builder.push(" WHEN 'large_airport' THEN 1 ");
|
||||
builder.push(" WHEN 'medium_airport' THEN 2 ");
|
||||
builder.push(" WHEN 'small_airport' THEN 3 ");
|
||||
builder.push(" WHEN 'seaplane_base' THEN 4 ");
|
||||
builder.push(" WHEN 'heliport' THEN 5 ");
|
||||
builder.push(" WHEN 'balloon_port' THEN 6 ");
|
||||
builder.push(" WHEN 'unknown' THEN 7 ");
|
||||
builder.push(" ELSE 8 END");
|
||||
|
||||
// Apply pagination.
|
||||
if let Some(limit) = query.limit {
|
||||
@@ -361,8 +406,12 @@ impl Airport {
|
||||
&query.municipalities,
|
||||
);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "local", &query.locals);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "name", &query.names);
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "category", &query.categories);
|
||||
Self::push_condition_like(&mut builder, &mut has_where, "name", &query.name);
|
||||
if let Err(err) = Self::push_condition_bounds(&mut builder, &mut has_where, &query.bounds) {
|
||||
log::error!("Error parsing bounds string: {}", err);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let sql_query = builder.build_query_scalar();
|
||||
sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0)
|
||||
@@ -529,4 +578,56 @@ impl Airport {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_condition_like<'a>(
|
||||
builder: &mut QueryBuilder<'a, Postgres>,
|
||||
has_where: &mut bool,
|
||||
column: &str,
|
||||
field: &'a Option<String>,
|
||||
) {
|
||||
// Query column like
|
||||
if let Some(ref value) = field {
|
||||
if !*has_where {
|
||||
builder.push(" WHERE ");
|
||||
*has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
// Using ILIKE with wildcards for partial matching
|
||||
builder
|
||||
.push(column)
|
||||
.push(" ILIKE ")
|
||||
.push_bind(format!("%{}%", value));
|
||||
}
|
||||
}
|
||||
|
||||
fn push_condition_bounds<'a>(
|
||||
builder: &mut QueryBuilder<'a, Postgres>,
|
||||
has_where: &mut bool,
|
||||
field: &'a Option<String>,
|
||||
) -> ApiResult<()> {
|
||||
// Query bounds
|
||||
if let Some(ref bounds_string) = field {
|
||||
if !*has_where {
|
||||
builder.push(" WHERE ");
|
||||
*has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
let bounds = Bounds::parse(bounds_string)?;
|
||||
builder
|
||||
.push("(")
|
||||
.push("latitude BETWEEN ")
|
||||
.push_bind(bounds.south_west_lat)
|
||||
.push(" AND ")
|
||||
.push_bind(bounds.north_east_lat)
|
||||
.push(" AND ")
|
||||
.push("longitude BETWEEN ")
|
||||
.push_bind(bounds.south_west_lon)
|
||||
.push(" AND ")
|
||||
.push_bind(bounds.north_east_lon)
|
||||
.push(")");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::OnceLock;
|
||||
use actix_web::{
|
||||
post, web, HttpResponse, ResponseError,
|
||||
cookie::{Cookie, time::Duration},
|
||||
HttpRequest,
|
||||
HttpRequest, put,
|
||||
};
|
||||
use crate::{
|
||||
auth::{verify_hash, Session, SESSION_COOKIE_NAME},
|
||||
@@ -10,7 +10,9 @@ use crate::{
|
||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||
};
|
||||
|
||||
use crate::auth::{Auth, DEFAULT_SESSION_TTL};
|
||||
use crate::auth::{hash, Auth, DEFAULT_SESSION_TTL};
|
||||
use crate::error::ApiResult;
|
||||
use crate::users::UpdateUser;
|
||||
|
||||
#[post("/register")]
|
||||
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||
@@ -132,21 +134,61 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||
}
|
||||
|
||||
#[post("/key")]
|
||||
async fn create_api_key(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
#[put("/password")]
|
||||
async fn change_password(
|
||||
password: web::Json<String>,
|
||||
req: HttpRequest,
|
||||
auth: Auth,
|
||||
) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let api_key = Session::new(128, &auth.user.email, &ip_address, None);
|
||||
let email = auth.user.email;
|
||||
|
||||
// TODO: store api key
|
||||
HttpResponse::Ok().body(api_key.session_id)
|
||||
if let None = User::select(&email).await {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
};
|
||||
|
||||
let update_user = UpdateUser {
|
||||
email: None,
|
||||
password: Some(password.into_inner()),
|
||||
role: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
};
|
||||
|
||||
match update_user.update(&email).await {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
log::info!(
|
||||
"Successful password change attempt [Email: {}] [IP Address: {}]",
|
||||
&email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Invalid password change attempt [Email: {}] [IP Address: {}]: {}",
|
||||
&email,
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
ResponseError::error_response(&Error::new(500, err.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/password-reset")]
|
||||
async fn password_reset(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("auth")
|
||||
web::scope("account")
|
||||
.service(register)
|
||||
.service(login)
|
||||
.service(logout)
|
||||
.service(create_api_key),
|
||||
.service(change_password),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,12 @@ impl From<core::num::ParseIntError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::num::ParseFloatError> for Error {
|
||||
fn from(error: core::num::ParseFloatError) -> Self {
|
||||
Self::new(500, format!("Parse error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::env::VarError> for Error {
|
||||
fn from(error: std::env::VarError) -> Self {
|
||||
Self::new(
|
||||
|
||||
@@ -33,6 +33,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
log::debug!("Creating default administrator");
|
||||
let password = admin_password.unwrap();
|
||||
let password_hash = hash(&password)?;
|
||||
if email == "admin@example.com" || password == "CHANGEME" {
|
||||
log::warn!(
|
||||
"Default admin credentials are in use, update the ADMIN_EMAIL and ADMIN_PASSWORD."
|
||||
);
|
||||
}
|
||||
let admin_user = User {
|
||||
email,
|
||||
password_hash,
|
||||
|
||||
@@ -216,7 +216,7 @@ impl MetarRow {
|
||||
raw_text,
|
||||
data
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
TABLE_NAME,
|
||||
))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use crate::{auth::hash, error::ApiResult};
|
||||
use crate::db;
|
||||
|
||||
@@ -8,9 +8,6 @@ pub const ADMIN_ROLE: &str = "ADMIN";
|
||||
pub const USER_ROLE: &str = "USER";
|
||||
const TABLE_NAME: &str = "users";
|
||||
|
||||
/**
|
||||
* RegisterRequest
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
@@ -34,18 +31,12 @@ impl RegisterRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginRequest
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/**
|
||||
* UserResponse
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserResponse {
|
||||
pub email: String,
|
||||
@@ -65,6 +56,75 @@ impl From<User> for UserResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct UpdateUser {
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateUser {
|
||||
pub async fn update(&self, email: &str) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
|
||||
let mut query_builder: QueryBuilder<Postgres> =
|
||||
QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME));
|
||||
|
||||
let mut first_clause = true;
|
||||
|
||||
let mut push_comma = |query_builder: &mut QueryBuilder<Postgres>| {
|
||||
if !first_clause {
|
||||
query_builder.push(", ");
|
||||
} else {
|
||||
first_clause = false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref email) = self.email {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("email = ");
|
||||
query_builder.push_bind(email);
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
push_comma(&mut query_builder);
|
||||
let password_hash = hash(password)?;
|
||||
query_builder.push("password_hash = ");
|
||||
query_builder.push_bind(password_hash);
|
||||
}
|
||||
if let Some(ref role) = self.role {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("role = ");
|
||||
query_builder.push_bind(role);
|
||||
}
|
||||
if let Some(ref first_name) = self.first_name {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("first_name = ");
|
||||
query_builder.push_bind(first_name);
|
||||
}
|
||||
if let Some(ref last_name) = self.last_name {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("last_name = ");
|
||||
query_builder.push_bind(last_name);
|
||||
}
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("updated_at = ");
|
||||
query_builder.push_bind(Utc::now());
|
||||
|
||||
query_builder.push(" WHERE email = ");
|
||||
query_builder.push_bind(email.to_string());
|
||||
query_builder.push(" RETURNING *");
|
||||
|
||||
dbg!(&query_builder.sql());
|
||||
|
||||
let query = query_builder.build_query_as::<User>();
|
||||
let user = query.fetch_one(pool).await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)]
|
||||
pub struct User {
|
||||
pub email: String,
|
||||
|
||||
Reference in New Issue
Block a user