Fixed import
This commit is contained in:
25
service/Cargo.lock
generated
25
service/Cargo.lock
generated
@@ -793,9 +793,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@@ -803,9 +803,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
@@ -826,9 +826,9 @@ checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -837,21 +837,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -1801,6 +1801,7 @@ dependencies = [
|
|||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ jsonwebtoken = "9.0.0"
|
|||||||
redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] }
|
redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] }
|
||||||
rustix = "0.38.19" # https://github.com/imsnif/bandwhich/issues/284
|
rustix = "0.38.19" # https://github.com/imsnif/bandwhich/issues/284
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
|
futures-util = "0.3.29"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ services:
|
|||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
SERVICE_HOST: service
|
SERVICE_HOST: service
|
||||||
SERVICE_PORT: 5000
|
SERVICE_PORT: 5000
|
||||||
|
KEYS_DIR_PATH: /keys
|
||||||
volumes:
|
volumes:
|
||||||
- ${KEYS_DIR_PATH}:/keys
|
- ${KEYS_DIR_PATH}:/keys
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -248,6 +248,19 @@ impl QueryAirport {
|
|||||||
Ok(airport)
|
Ok(airport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_all (airports: Vec<Self>) -> Result<Vec<Self>, ServiceError> {
|
||||||
|
let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> = db::connection()?;
|
||||||
|
let mut inserted_airports: Vec<Self> = vec![];
|
||||||
|
for airport in airports {
|
||||||
|
let airport = Self::from(airport);
|
||||||
|
let airport = diesel::insert_into(airports::table)
|
||||||
|
.values(airport)
|
||||||
|
.get_result(&mut conn)?;
|
||||||
|
inserted_airports.push(airport);
|
||||||
|
}
|
||||||
|
Ok(inserted_airports)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(icao: String, airport: Self) -> Result<Self, ServiceError> {
|
pub fn update(icao: String, airport: Self) -> Result<Self, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
let mut conn = db::connection()?;
|
||||||
let airport = diesel::update(airports::table)
|
let airport = diesel::update(airports::table)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use futures_util::stream::StreamExt as _;
|
||||||
|
|
||||||
use crate::{airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport}, db::{self, Response, Metadata}, auth::{JwtAuth, verify_role}};
|
use crate::{airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport}, db::{Response, Metadata}, auth::{JwtAuth, verify_role}};
|
||||||
|
use actix_multipart::Multipart;
|
||||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use postgis_diesel::types::{Polygon, Point};
|
use postgis_diesel::types::{Polygon, Point};
|
||||||
@@ -18,16 +20,48 @@ struct GetAllParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/import")]
|
#[post("/import")]
|
||||||
async fn import(auth: JwtAuth) -> HttpResponse {
|
async fn import(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, "admin") {
|
if let Err(err) = verify_role(&auth, "admin") {
|
||||||
Ok(_) => {},
|
return ResponseError::error_response(&err)
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
|
||||||
};
|
};
|
||||||
let count = db::import_data();
|
|
||||||
HttpResponse::Ok().json(Response {
|
|
||||||
data: count,
|
while let Some(item) = payload.next().await {
|
||||||
meta: None
|
let mut bytes = web::BytesMut::new();
|
||||||
})
|
let mut field = match item {
|
||||||
|
Ok(field) => field,
|
||||||
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build bytes from chunks
|
||||||
|
while let Some(chunk) = field.next().await {
|
||||||
|
let data = match chunk {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get chunk: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bytes.extend_from_slice(&data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert bytes to Vec<Airport>
|
||||||
|
let airports: Vec<Airport> = match serde_json::from_slice(&bytes) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to parse JSON: {}", err);
|
||||||
|
return ResponseError::error_response(&err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert Vec<Airport> to Vec<QueryAirport> and insert into database
|
||||||
|
let query_airports: Vec<QueryAirport> = airports.into_iter().map(|a| a.into()).collect();
|
||||||
|
match QueryAirport::insert_all(query_airports) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("")]
|
#[get("")]
|
||||||
|
|||||||
@@ -127,6 +127,41 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/session")]
|
||||||
|
async fn session(req: HttpRequest) -> HttpResponse {
|
||||||
|
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||||
|
// If there is a access_token cookie, check if it is valid
|
||||||
|
let has_session = match req.cookie("access_token") {
|
||||||
|
Some(cookie) => {
|
||||||
|
let access_token = cookie.value().to_string();
|
||||||
|
let public_key = std::fs::read_to_string(format!("{}access_public_key.pem", keys_dir))
|
||||||
|
.expect("Unable to read refresh public key");
|
||||||
|
match verify_token(&access_token, &public_key) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => false
|
||||||
|
};
|
||||||
|
if !has_session {
|
||||||
|
// If there is a refresh_token cookie, check if it is valid
|
||||||
|
match req.cookie("refresh_token") {
|
||||||
|
Some(cookie) => {
|
||||||
|
let refresh_token = cookie.value().to_string();
|
||||||
|
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||||
|
.expect("Unable to read refresh public key");
|
||||||
|
match verify_token(&refresh_token, &public_key) {
|
||||||
|
Ok(_) => return HttpResponse::Ok().json(true),
|
||||||
|
Err(_) => return HttpResponse::Ok().json(false)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
None => return HttpResponse::Ok().json(false)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return HttpResponse::Ok().json(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct RefreshParams {
|
struct RefreshParams {
|
||||||
refresh_token_rotation: Option<bool>
|
refresh_token_rotation: Option<bool>
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ pub fn update_airports() {
|
|||||||
let mut peekable = airport_icaos.into_iter().peekable();
|
let mut peekable = airport_icaos.into_iter().peekable();
|
||||||
let mut observation_time = chrono::Utc::now().timestamp();
|
let mut observation_time = chrono::Utc::now().timestamp();
|
||||||
|
|
||||||
|
if peekable.peek().is_none() {
|
||||||
|
sleep(Duration::from_secs(3600)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
while peekable.peek().is_some() {
|
while peekable.peek().is_some() {
|
||||||
let chunk: Vec<String> = peekable.by_ref().take(limit as usize).collect();
|
let chunk: Vec<String> = peekable.by_ref().take(limit as usize).collect();
|
||||||
let icao_string = chunk.join(",");
|
let icao_string = chunk.join(",");
|
||||||
@@ -58,7 +63,7 @@ pub fn update_airports() {
|
|||||||
// Sleep until the earliest observation time is 1 hour old
|
// Sleep until the earliest observation time is 1 hour old
|
||||||
// Bounded by 1 and 3600 seconds
|
// Bounded by 1 and 3600 seconds
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
let sleep_time = std::cmp::min(std::cmp::max(1, now - (observation_time + (3600))), 3600);
|
let sleep_time = std::cmp::min(std::cmp::max(1, now - (observation_time + 3600)), 3600);
|
||||||
debug!("Next update in {} seconds", sleep_time);
|
debug!("Next update in {} seconds", sleep_time);
|
||||||
sleep(Duration::from_secs(sleep_time as u64)).await;
|
sleep(Duration::from_secs(sleep_time as u64)).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ export async function updateAirport({ airport }: { airport: Airport }): Promise<
|
|||||||
return response?.json() || { data: undefined };
|
return response?.json() || { data: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importAirports(): Promise<any> {
|
export async function importAirports(payload: File): Promise<any> {
|
||||||
const response = await postRequest('airports/import');
|
const data = new FormData();
|
||||||
return response?.json() || { data: undefined };
|
data.append('data', payload);
|
||||||
|
const response = await postRequest('airports/import', data, {
|
||||||
|
type: 'form'
|
||||||
|
});
|
||||||
|
return response?.status == 200;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getAirports, importAirports, removeAirport } from "@/api/airport";
|
import { getAirports, importAirports, removeAirport } from "@/api/airport";
|
||||||
import { Airport, airportCategoryToText } from "@/api/airport.types";
|
import { Airport, airportCategoryToText } from "@/api/airport.types";
|
||||||
import { Text, Button, Card, Group, Pagination, Table, TextInput, rem, UnstyledButton, Center, Flex, Container, Grid, Space } from "@mantine/core";
|
import { Text, Button, Card, Group, Pagination, Table, TextInput, rem, UnstyledButton, Center, Flex, Container, Grid, Space, FileButton } from "@mantine/core";
|
||||||
import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi";
|
import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CiSearch } from "react-icons/ci";
|
import { CiSearch } from "react-icons/ci";
|
||||||
@@ -81,12 +81,14 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
<Grid.Col span={2}>
|
<Grid.Col span={2}>
|
||||||
<Flex justify={'end'}>
|
<Flex justify={'end'}>
|
||||||
<Space mr={'sm'}>
|
<Space mr={'sm'}>
|
||||||
<PanelButton onClick={async () => {
|
<PanelFileButton accept={'.json'} onChange={async (payload) => {
|
||||||
await importAirports();
|
if (payload instanceof File) {
|
||||||
await getAirportData();
|
await importAirports(payload);
|
||||||
|
await getAirportData();
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
Import
|
Import
|
||||||
</PanelButton>
|
</PanelFileButton>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<PanelButton color={'red'} onClick={async () => {
|
<PanelButton color={'red'} onClick={async () => {
|
||||||
@@ -102,7 +104,35 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelButton({ children, color = 'blue', onClick }: {children: any, color?: string, onClick: () => Promise<void> }) {
|
interface PanelButtonProps {
|
||||||
|
children: any;
|
||||||
|
color?: string;
|
||||||
|
onClick?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PanelFileButtonProps {
|
||||||
|
children: any;
|
||||||
|
color?: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
accept?: string;
|
||||||
|
onChange?: (payload: File|File[]|null) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PanelFileButton({ children, multiple = false, accept, color, onChange = async () => {} }: PanelFileButtonProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
return <FileButton
|
||||||
|
multiple={multiple}
|
||||||
|
accept={accept}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLoading(true);
|
||||||
|
onChange(e).then(() => setLoading(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(props) => <Button loading={loading} variant='light' color={color} radius='md' {...props}>{children}</Button>}
|
||||||
|
</FileButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PanelButton({ children, color = 'blue', onClick = async () => {} }: PanelButtonProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
return <Button
|
return <Button
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
Reference in New Issue
Block a user