Fixed docker issue temporarily

This commit is contained in:
2025-04-11 23:02:47 -04:00
parent 05b5ceafe2
commit 74fa7da751
18 changed files with 145 additions and 92 deletions

10
.env
View File

@@ -2,8 +2,7 @@ RUST_LOG=warn,api=info
HTTPD_DOMAIN=localhost
HTTPD_PROTOCOL=http
HTTPD_HTTP_PORT=8080
HTTPD_HTTPS_PORT=8443
HTTPD_PORT=8080
HTTPD_MINIO_HOST=host.docker.internal
HTTPD_API_HOST=host.docker.internal
HTTPD_UI_HOST=host.docker.internal
@@ -24,18 +23,19 @@ MINIO_BUCKET=aviation
MINIO_PROTOCOL=http
MINIO_PORT=9000
MINIO_PORT_INTERNAL=9001
MINIO_BROWSER_REDIRECT_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/minio/
MINIO_BROWSER_REDIRECT_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_PORT}/minio/
UI_PROTOCOL=http
UI_PORT=3000
API_PROTOCOL=http
API_HOST=0.0.0.0
API_PORT=5000
VITE_API_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/api
VITE_API_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_PORT}/api
ENVIRONMENT=development
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=CHANGEME
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
AVIATION_WEATHER_URL=https://aviationweather.gov/api/data

View File

@@ -11,26 +11,14 @@ COPY Cargo.toml ./
RUN apt-get update && apt-get install -y cmake
RUN cargo build --release
# ======
# Keys
# ======
FROM debian:bookworm-slim AS keys
WORKDIR /keys
RUN apt-get update && apt-get install -y openssl libpq-dev
RUN openssl genrsa -out access.pem 4096
RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub
RUN openssl genrsa -out refresh.pem 4096
RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub
# =========
# Runtime
# =========
FROM keys AS runtime
FROM debian:bookworm-slim AS runtime
WORKDIR /api
RUN apt-get update && apt-get install -y openssl libpq-dev
USER root
COPY --from=builder /builder/target/release/api /usr/local/bin/api
COPY --from=keys /keys /keys
CMD ["api"]

View File

@@ -17,6 +17,15 @@ CREATE TABLE IF NOT EXISTS airports (
public BOOLEAN DEFAULT false
);
CREATE INDEX ON airports (iata);
CREATE INDEX ON airports (local);
CREATE INDEX ON airports (name);
CREATE INDEX ON airports (category);
CREATE INDEX ON airports (iso_country);
CREATE INDEX ON airports (iso_region);
CREATE INDEX ON airports (municipality);
CREATE INDEX ON airports (longitude, latitude);
CREATE TABLE IF NOT EXISTS runways (
id UUID PRIMARY KEY NOT NULL,
icao TEXT NOT NULL,
@@ -26,6 +35,9 @@ CREATE TABLE IF NOT EXISTS runways (
surface TEXT NOT NULL
);
CREATE INDEX ON runways (icao);
CREATE INDEX ON runways (surface);
CREATE TABLE IF NOT EXISTS frequencies (
id UUID PRIMARY KEY NOT NULL,
icao TEXT NOT NULL,
@@ -33,6 +45,9 @@ CREATE TABLE IF NOT EXISTS frequencies (
frequency_mhz REAL NOT NULL
);
CREATE INDEX ON frequencies (icao);
CREATE INDEX ON frequencies (frequency_mhz);
CREATE TABLE IF NOT EXISTS metars (
icao TEXT NOT NULL,
observation_time TIMESTAMPTZ NOT NULL,
@@ -40,6 +55,8 @@ CREATE TABLE IF NOT EXISTS metars (
data JSONB NOT NULL
);
CREATE INDEX ON metars (observation_time DESC);
CREATE TABLE IF NOT EXISTS users (
email TEXT PRIMARY KEY NOT NULL,
password_hash TEXT NOT NULL,

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::str::FromStr;
use futures_util::try_join;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder};
use crate::airports::{
@@ -194,7 +195,7 @@ impl From<AirportRow> for Airport {
}
impl Airport {
pub async fn select(icao: &str, metar: bool) -> Option<Self> {
pub async fn select(client: &Client, icao: &str, metar: bool) -> Option<Self> {
let pool = db::pool();
let airport_fut = async {
@@ -206,7 +207,7 @@ impl Airport {
let metar_fut = async {
if metar {
match Metar::find_all(&vec![icao.to_string()]).await {
match Metar::find_all(client, &vec![icao.to_string()], &false).await {
Ok(m) => Some(m.into_iter().nth(0)),
Err(err) => {
log::error!("{}", err);
@@ -269,7 +270,7 @@ impl Airport {
})
}
pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> {
pub async fn select_all(client: &Client, query: &AirportQuery) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM ");
@@ -337,7 +338,7 @@ impl Airport {
let runway_future = Runway::select_all_map(icaos.clone());
let frequency_future = Frequency::select_all_map(icaos.clone());
let metar_future = if query.metars.unwrap_or(false) {
Some(Metar::find_all(&icaos))
Some(Metar::find_all(client, &icaos, &false))
} else {
None
};

View File

@@ -4,6 +4,7 @@ use crate::{
airports::Airport,
db::Paged,
auth::{Auth, verify_role},
AppState,
};
use actix_multipart::Multipart;
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
@@ -53,7 +54,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
}
#[get("")]
async fn get_airports(req: HttpRequest) -> HttpResponse {
async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
Ok(q) => q.into_inner(),
Err(err) => {
@@ -71,7 +72,8 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
query.limit = Some(limit);
query.page = Some(page);
match Airport::select_all(&query).await {
let client = &data.client;
match Airport::select_all(client, &query).await {
Ok(airports) => HttpResponse::Ok().json(Paged {
data: airports,
page,
@@ -86,7 +88,11 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
}
#[get("/{icao}")]
async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse {
async fn get_airport(
data: web::Data<AppState>,
icao: web::Path<String>,
req: HttpRequest,
) -> HttpResponse {
let metar = match web::Query::<AirportQuery>::from_query(req.query_string()) {
Ok(q) => q.metars.unwrap_or_else(|| false),
Err(err) => {
@@ -95,7 +101,8 @@ async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse
}
};
match Airport::select(&icao.into_inner(), metar).await {
let client = &data.client;
match Airport::select(client, &icao.into_inner(), metar).await {
Some(airport) => HttpResponse::Ok().json(airport),
None => HttpResponse::NotFound().finish(),
}

View File

@@ -97,7 +97,18 @@ impl From<std::env::VarError> for Error {
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::new(500, format!("Unknown reqwest error: {}", error))
match error.status() {
Some(status_code) => {
if status_code.is_client_error() {
Self::new(500, format!("Client reqwest error: {}", error))
} else if status_code.is_server_error() {
Self::new(500, format!("Server reqwest error: {}", error))
} else {
Self::new(500, format!("Unknown reqwest error: {:?}", error))
}
}
_ => Self::new(500, format!("Unknown reqwest error: {:?}", error)),
}
}
}

View File

@@ -1,5 +1,5 @@
use std::env;
use std::time::Duration;
use actix_cors::Cors;
use actix_web::{App, HttpServer, middleware::Logger, web};
use dotenv::from_filename;
@@ -14,15 +14,17 @@ mod metars;
mod scheduler;
mod users;
#[derive(Debug, Clone)]
struct AppState {
client: reqwest::Client,
}
#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
initialize_environment()?;
db::initialize().await?;
// scheduler::update_airports();
let host = "0.0.0.0".to_string();
let port = env::var("API_PORT").unwrap_or("5000".to_string());
// Initialize admin user
let admin_email = env::var("ADMIN_EMAIL");
let admin_password = env::var("ADMIN_PASSWORD");
@@ -55,6 +57,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.no_proxy()
.danger_accept_invalid_certs(true)
.build()
.expect("Failed to create reqwest client");
let state = AppState { client };
let host = env::var("API_HOST").unwrap_or("localhost".to_string());
let port = env::var("API_PORT").unwrap_or("5000".to_string());
let server = match HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
@@ -62,7 +75,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.allow_any_header()
.supports_credentials()
.max_age(3600);
App::new().wrap(cors).wrap(Logger::default()).service(
App::new()
.wrap(cors)
.wrap(Logger::default())
.app_data(web::Data::new(state.clone()))
.service(
web::scope("api")
.configure(airports::init_routes)
.configure(metars::init_routes)
@@ -73,7 +90,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.bind(format!("{}:{}", host, port))
{
Ok(b) => {
log::info!("Binding server to {}:{}", host, port);
log::info!("Server bound to {}:{}", host, port);
b
}
Err(err) => {

View File

@@ -3,6 +3,7 @@ use crate::{error::ApiResult, db};
use chrono::{DateTime, Datelike, Utc};
use std::collections::HashSet;
use redis::{AsyncCommands, RedisResult};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::db::redis_async_connection;
@@ -845,8 +846,8 @@ impl Metar {
missing_metar_icaos
}
async fn get_remote_metars(icaos: &[&str]) -> ApiResult<Vec<Metar>> {
let gov_api_url = std::env::var("GOV_API_URL").expect("GOV_API_URL must be set");
async fn get_remote_metars(client: &Client, icaos: &[&str]) -> ApiResult<Vec<Metar>> {
let base_url = std::env::var("AVIATION_WEATHER_URL").expect("GOV_API_URL must be set");
// Query the remote API for the missing METAR data 10 at a time
let icao_chunks = icaos
.chunks(10)
@@ -854,14 +855,14 @@ impl Metar {
.collect::<Vec<String>>();
let mut metars: Vec<Metar> = vec![];
for icao_chunk in icao_chunks {
let url = format!("{}/metar.php?ids={}", gov_api_url, icao_chunk);
let mut m = match reqwest::get(url).await {
let url = format!("{}/metar?ids={}&order=id", base_url, icao_chunk);
let mut m = match client.get(url).send().await {
Ok(r) => {
// Check if the status code is 200
if r.status() != 200 {
return Err(Error::new(
500,
format!("Unable to get METAR request: {}", r.status()),
format!("Request returned status {}", r.status()),
));
}
match r.text().await {
@@ -876,20 +877,10 @@ impl Metar {
Err(err) => return Err(err),
}
}
Err(err) => {
return Err(Error::new(
500,
format!("Unable to parse METAR request: {}", err),
))
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
}
}
}
Err(err) => {
return Err(Error::new(
500,
format!("Unable to get METAR request: {}", err),
))
}
Err(err) => return Err(err.into()),
};
metars.append(&mut m);
}
@@ -911,7 +902,11 @@ impl Metar {
})
}
pub async fn find_all(icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
pub async fn find_all(
client: &Client,
icao_list: &Vec<String>,
force: &bool,
) -> ApiResult<Vec<Self>> {
if icao_list.is_empty() {
return Ok(Vec::new());
}
@@ -937,6 +932,9 @@ impl Metar {
if !missing_icao_list.is_empty() {
let mut updated_missing_icao_list: Vec<&str> = Vec::new();
for icao in &missing_icao_list {
if *force {
updated_missing_icao_list.push(icao);
} else {
let result: RedisResult<Option<bool>> = conn.get(icao).await;
match result {
Ok(Some(value)) => {
@@ -950,12 +948,13 @@ impl Metar {
Err(err) => return Err(err.into()),
}
}
}
if !updated_missing_icao_list.is_empty() {
log::trace!(
"Retrieving missing METAR data for {:?}",
updated_missing_icao_list
);
let mut missing_icao_list = Self::get_remote_metars(&updated_missing_icao_list)
let mut missing_icao_list = Self::get_remote_metars(client, &updated_missing_icao_list)
.await
.unwrap_or_else(|err| {
log::warn!("Unable to get remote METAR data; {}", err);

View File

@@ -2,14 +2,16 @@ use crate::metars::Metar;
use actix_web::{get, web, HttpResponse, HttpRequest};
use log::error;
use serde::{Deserialize, Serialize};
use crate::AppState;
#[derive(Debug, Serialize, Deserialize)]
struct FindAllParameters {
icaos: Option<String>,
force: Option<bool>,
}
#[get("metars")]
async fn find_all(req: HttpRequest) -> HttpResponse {
async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let parameters = web::Query::<FindAllParameters>::from_query(req.query_string()).unwrap();
let icao_option = &parameters.icaos;
let icao_string = match icao_option {
@@ -17,8 +19,10 @@ async fn find_all(req: HttpRequest) -> HttpResponse {
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
};
let icaos: Vec<String> = icao_string.split(',').map(|s| s.to_string()).collect();
let force = &parameters.force.unwrap_or(false);
let metars = match Metar::find_all(&icaos).await {
let client = &data.client;
let metars = match Metar::find_all(client, &icaos, force).await {
Ok(a) => a,
Err(err) => {
error!("{}", err);

View File

@@ -5,11 +5,12 @@ meta {
}
get {
url: {{API_URL}}/metars?icaos=KJYO,KOKV,KMRB,KHEF,KIAD
url: {{API_URL}}/metars?icaos=KJYO,KOKV,KMRB,KHEF,KIAD&force=true
body: none
auth: none
}
params:query {
icaos: KJYO,KOKV,KMRB,KHEF,KIAD
force: true
}

View File

@@ -1,3 +1,6 @@
vars {
BASE_URL: http://localhost:8080
~BASE_URL: http://localhost:5000
~BASE_URL: http://127.0.0.1:5000
~BASE_URL: http://[::1]:5000
}

View File

@@ -4,6 +4,9 @@ x-env_file: &env
- path: .env.local
required: false
x-restart: &default_restart
restart: unless-stopped
name: aviation
services:
httpd:
@@ -14,12 +17,11 @@ services:
dockerfile: Dockerfile
env_file: *env
ports:
- "${HTTPD_HTTP_PORT:-8080}:80"
- "${HTTPD_HTTPS_PORT:-8443}:443"
- "${HTTPD_PORT:-8080}:80"
networks:
- frontend
- backend
restart: unless-stopped
<<: *default_restart
postgres:
image: postgis/postgis:17-3.5
@@ -38,7 +40,7 @@ services:
- backend
profiles:
- backend
restart: unless-stopped
<<: *default_restart
redis:
image: redis:8.0-M03 # Replace with valkey?
@@ -56,7 +58,7 @@ services:
- backend
profiles:
- backend
restart: unless-stopped
<<: *default_restart
minio:
image: minio/minio:RELEASE.2025-02-28T09-55-16Z
@@ -76,7 +78,7 @@ services:
profiles:
- backend
command: server --console-address ":9001" /data
restart: unless-stopped
<<: *default_restart
api:
image: aviation-api:latest
@@ -86,6 +88,7 @@ services:
dockerfile: Dockerfile
env_file: *env
environment:
API_HOST: 0.0.0.0
POSTGRES_HOST: aviation-postgres
POSTGRES_PORT: 5432
REDIS_HOST: aviation-redis
@@ -103,7 +106,7 @@ services:
- backend
profiles:
- api
restart: unless-stopped
<<: *default_restart
ui:
image: aviation-ui:latest
@@ -125,7 +128,7 @@ services:
profiles:
- frontend
command: ["npm", "run", "dev"]
restart: unless-stopped
<<: *default_restart
volumes:
postgres:

View File

@@ -158,14 +158,14 @@ LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule session_dbd_module modules/mod_session_dbd.so
#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
LoadModule ssl_module modules/mod_ssl.so
#LoadModule ssl_module modules/mod_ssl.so
#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
#LoadModule dialup_module modules/mod_dialup.so
LoadModule http2_module modules/mod_http2.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so
#LoadModule http2_module modules/mod_http2.so
#LoadModule proxy_http2_module modules/mod_proxy_http2.so
#LoadModule md_module modules/mod_md.so
#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
@@ -229,7 +229,7 @@ Group www-data
# e-mailed. This address appears on some server-generated pages, such
# as error documents. e.g. admin@your-domain.com
#
ServerAdmin you@example.com
ServerAdmin ben@bensherrif.com
#
# ServerName gives the name and port that the server uses to identify itself.

View File

@@ -50,7 +50,9 @@ body,
cursor: pointer;
user-select: none;
transition: background-color 0.2s, color 0.2s;
transition:
background-color 0.2s,
color 0.2s;
}
.map-button.active {

View File

@@ -42,14 +42,14 @@ function App() {
useEffect(() => {
if (showRadar) {
getWeatherMapUrl().then(url => {
getWeatherMapUrl().then((url) => {
setRainViewerUrl(url);
});
}
}, [showRadar]);
function toggleRadar() {
setShowRadar(prev => {
setShowRadar((prev) => {
const newValue = !prev;
Cookies.set('showRadar', newValue.toString(), { expires: 7 });
return newValue;
@@ -96,7 +96,7 @@ function App() {
<TileLayer url={darkLayerUrl} />
</LayersControl.BaseLayer>
</LayersControl>
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={5} />}
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={10} />}
<ZoomControl position={'bottomright'} />
<AirportLayer setAirport={setAirport} />
<BaseLayerChangeHandler />

View File

@@ -27,7 +27,7 @@ export default function AirportMarker({
mouseout: () => markerRef.current?.closePopup()
}}
>
<Popup closeButton={false}>
<Popup closeButton={false} autoPan={false}>
{airport.icao} - {airport.name}
</Popup>
</Marker>

View File

@@ -4,7 +4,7 @@ const weatherMapsUrl = 'https://api.rainviewer.com/public/weather-maps.json';
async function getWeatherMaps(): Promise<WeatherMaps | undefined> {
const response = await fetch(`${weatherMapsUrl}`, {
method: 'GET',
method: 'GET'
});
if (response?.status === 200) {
return response.json();
@@ -18,13 +18,13 @@ export async function getWeatherMapUrl(): Promise<string | null> {
if (weatherMaps != undefined) {
let url = weatherMaps.host;
// url = 'https://cdn.rainviewer.com';
let latest = "";
let latest = '';
if (weatherMaps.radar.past.length > 0) {
latest = weatherMaps.radar.past[weatherMaps.radar.past.length - 1].path;
} else {
return null;
}
url += latest + "/256/{z}/{x}/{y}/2/1_1.png";
url += latest + '/256/{z}/{x}/{y}/2/1_1.png';
// url += latest + "/256/{z}/{x}/{y}/255/1_1_1_0.webp";
return url;
} else {