updating api formats

This commit is contained in:
2023-09-16 22:39:02 -04:00
parent 7d9d32558f
commit 4a3225c3f9
23 changed files with 522 additions and 79 deletions

View File

@@ -45,7 +45,7 @@ dependencies = [
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"ahash", "ahash",
"base64", "base64 0.21.4",
"bitflags 2.4.0", "bitflags 2.4.0",
"brotli", "brotli",
"bytes", "bytes",
@@ -73,6 +73,22 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "actix-identity"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1224c9f9593dc27c9077b233ce04adedc1d7febcfc35ee9f53ea3c24df180bec"
dependencies = [
"actix-service",
"actix-session",
"actix-utils",
"actix-web",
"anyhow",
"futures-core",
"serde",
"tracing",
]
[[package]] [[package]]
name = "actix-macros" name = "actix-macros"
version = "0.2.4" version = "0.2.4"
@@ -135,6 +151,23 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-session"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"anyhow",
"async-trait",
"derive_more",
"serde",
"serde_json",
"tracing",
]
[[package]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.1" version = "3.0.1"
@@ -212,6 +245,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.3" version = "0.8.3"
@@ -263,6 +331,23 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "async-trait"
version = "0.1.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@@ -284,6 +369,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.4" version = "0.21.4"
@@ -390,6 +481,16 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -402,7 +503,14 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [ dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding", "percent-encoding",
"rand",
"sha2",
"subtle",
"time", "time",
"version_check", "version_check",
] ]
@@ -448,9 +556,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.8" version = "0.3.8"
@@ -526,6 +644,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@@ -689,6 +808,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.0" version = "0.28.0"
@@ -732,6 +861,24 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hkdf"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@@ -862,6 +1009,15 @@ dependencies = [
"hashbrown 0.14.0", "hashbrown 0.14.0",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.8.0" version = "2.8.0"
@@ -1068,6 +1224,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.57" version = "0.10.57"
@@ -1165,6 +1327,18 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "polyval"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "postgis_diesel" name = "postgis_diesel"
version = "2.2.1" version = "2.2.1"
@@ -1304,7 +1478,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [ dependencies = [
"base64", "base64 0.21.4",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -1485,6 +1659,17 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha2"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@@ -1529,6 +1714,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -1763,6 +1954,16 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.4.1" version = "2.4.1"
@@ -1882,6 +2083,7 @@ name = "weather-service"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-identity",
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"chrono", "chrono",

View File

@@ -8,6 +8,8 @@ edition = "2021"
[dependencies] [dependencies]
actix-web = "4.4.0" actix-web = "4.4.0"
actix-rt = "2.9.0" actix-rt = "2.9.0"
actix-cors = "0.6.4"
actix-identity = "0.5.0"
chrono = { version = "0.4.30", features = ["serde"] } chrono = { version = "0.4.30", features = ["serde"] }
dotenv = "0.15.0" dotenv = "0.15.0"
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] } diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] }
@@ -24,4 +26,3 @@ serde_json = "1.0.105"
tokio = { version = "1.32.0", features = ["macros", "rt"] } tokio = { version = "1.32.0", features = ["macros", "rt"] }
uuid = { version = "1.4.1", features = ["serde", "v4"] } uuid = { version = "1.4.1", features = ["serde", "v4"] }
log = "0.4.20" log = "0.4.20"
actix-cors = "0.6.4"

View File

@@ -27,7 +27,7 @@ clean:
rm -rf target rm -rf target
clean-db: ## Remove database and Cargo packages clean-db: ## Remove database and Cargo packages
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' || true docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"'
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true

56
weather-service/README.md Normal file
View File

@@ -0,0 +1,56 @@
## REST API
The REST API for the weather service is described below.
### Import Airports
---
#### Request
`GET /import`
#### Response
```
```
### Get Airports
---
#### Request
`GET /airports`
#### Response
```
```
### Get Airport
---
#### Request
`GET /airports/{icao}`
#### Response
```
```
### Create Airport
---
#### Request
`CREATE /airports`
#### Response
```
```
### Update Airport
---
#### Request
`PUT /airports/{icao}`
#### Response
```
```
### Delete Airport
---
#### Request
`DELETE /airports/{icao}`
#### Response
```
```

View File

@@ -0,0 +1 @@
DROP TABLE users;

View File

@@ -0,0 +1,11 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
email TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
favorites TEXT[]
);

View File

@@ -1,15 +1,15 @@
use crate::db; use crate::db;
use crate::error_handler::CustomError; use crate::error_handler::ServiceError;
use crate::schema::airports; use crate::schema::airports;
use diesel::prelude::*; use diesel::prelude::*;
use log::trace; // use log::trace;
use postgis_diesel::types::*; use postgis_diesel::types::*;
use postgis_diesel::functions::*; use postgis_diesel::functions::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, AsChangeset, Insertable)] #[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = airports)] #[diesel(table_name = airports)]
pub struct Airport { pub struct InsertAirport {
pub icao: String, pub icao: String,
pub category: String, pub category: String,
pub full_name: String, pub full_name: String,
@@ -26,7 +26,7 @@ pub struct Airport {
#[derive(Serialize, Deserialize, Queryable, QueryableByName)] #[derive(Serialize, Deserialize, Queryable, QueryableByName)]
#[diesel(table_name = airports)] #[diesel(table_name = airports)]
pub struct Airports { pub struct QueryAirport {
pub icao: String, pub icao: String,
pub id: i32, pub id: i32,
pub category: String, pub category: String,
@@ -42,13 +42,13 @@ pub struct Airports {
pub point: Point pub point: Point
} }
impl Airports { impl QueryAirport {
pub fn get_all(bounds: Option<Polygon<Point>>, category: Option<String>, filter: Option<String>, limit: i32, page: i32) -> Result<Vec<Self>, CustomError> { pub fn get_all(bounds: Option<Polygon<Point>>, category: Option<String>, filter: Option<String>, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
let mut conn = db::connection()?; let mut conn = db::connection()?;
let mut query = airports::table let mut query = airports::table
.limit(limit as i64) .limit(limit as i64)
.into_boxed(); .into_boxed();
query = query.filter(airports::id.gt(page * limit)); query = query.filter(airports::id.gt(std::cmp::max(1, page - 1) * limit));
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
query = query.filter(st_contains(bounds, airports::point)); query = query.filter(st_contains(bounds, airports::point));
@@ -62,28 +62,28 @@ impl Airports {
.or(airports::full_name.ilike(format!("%{}%", filter))) .or(airports::full_name.ilike(format!("%{}%", filter)))
) )
} }
let debug = diesel::debug_query::<diesel::pg::Pg, _>(&query); // let debug = diesel::debug_query::<diesel::pg::Pg, _>(&query);
trace!("{}", debug); // trace!("{}", debug);
let airports: Vec<Airports> = query.order(airports::category.asc()).load::<Airports>(&mut conn)?; let airports: Vec<QueryAirport> = query.order(airports::category.asc()).load::<QueryAirport>(&mut conn)?;
Ok(airports) Ok(airports)
} }
pub fn find(icao: String) -> Result<Self, CustomError> { pub fn find(icao: String) -> Result<Self, ServiceError> {
let mut conn = db::connection()?; let mut conn = db::connection()?;
let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?; let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?;
Ok(airport) Ok(airport)
} }
pub fn create(airport: Airport) -> Result<Self, CustomError> { pub fn create(airport: InsertAirport) -> Result<Self, ServiceError> {
let mut conn = db::connection()?; let mut conn = db::connection()?;
let airport = Airport::from(airport); let airport = InsertAirport::from(airport);
let airport = diesel::insert_into(airports::table) let airport = diesel::insert_into(airports::table)
.values(airport) .values(airport)
.get_result(&mut conn)?; .get_result(&mut conn)?;
Ok(airport) Ok(airport)
} }
pub fn update(id: i32, airport: Airport) -> Result<Self, CustomError> { pub fn update(id: i32, airport: InsertAirport) -> 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)
.filter(airports::id.eq(id)) .filter(airports::id.eq(id))
@@ -92,7 +92,7 @@ pub fn update(id: i32, airport: Airport) -> Result<Self, CustomError> {
Ok(airport) Ok(airport)
} }
pub fn delete(id: i32) -> Result<usize, CustomError> { pub fn delete(id: i32) -> Result<usize, ServiceError> {
let mut conn = db::connection()?; let mut conn = db::connection()?;
let res = diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?; let res = diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?;
Ok(res) Ok(res)

View File

@@ -1,4 +1,4 @@
use crate::{airports::{Airport, Airports}, db::{self, Metadata}}; use crate::{airports::{InsertAirport, QueryAirport}, db::{self, Metadata}};
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest}; use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest};
use log::{error, warn}; use log::{error, warn};
use postgis_diesel::types::{Polygon, Point}; use postgis_diesel::types::{Polygon, Point};
@@ -21,7 +21,7 @@ async fn import() -> HttpResponse {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AirportsResponse { pub struct AirportsResponse {
pub data: Vec<Airports>, pub data: Vec<QueryAirport>,
pub meta: Metadata pub meta: Metadata
} }
@@ -82,7 +82,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
None => None None => None
}; };
match web::block(move || Airports::get_all(polygon, category, filter, params.limit, params.page)).await.unwrap() { match web::block(move || QueryAirport::get_all(polygon, category, filter, params.limit, params.page)).await.unwrap() {
Ok(a) => HttpResponse::Ok().json(AirportsResponse { Ok(a) => HttpResponse::Ok().json(AirportsResponse {
data: a, data: a,
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
@@ -96,13 +96,13 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AirportResponse { pub struct AirportResponse {
pub data: Airports, pub data: QueryAirport,
pub meta: Metadata pub meta: Metadata
} }
#[get("/airports/{icao}")] #[get("/airports/{icao}")]
async fn get(icao: web::Path<String>) -> HttpResponse { async fn get(icao: web::Path<String>) -> HttpResponse {
match Airports::find(icao.into_inner()) { match QueryAirport::find(icao.into_inner()) {
Ok(a) => HttpResponse::Ok().json(AirportResponse { Ok(a) => HttpResponse::Ok().json(AirportResponse {
data: a, data: a,
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
@@ -115,8 +115,8 @@ async fn get(icao: web::Path<String>) -> HttpResponse {
} }
#[post("/airports")] #[post("/airports")]
async fn create(airport: web::Json<Airport>) -> HttpResponse { async fn create(airport: web::Json<InsertAirport>) -> HttpResponse {
match Airports::create(airport.into_inner()) { match QueryAirport::create(airport.into_inner()) {
Ok(a) => HttpResponse::Created().json(a), Ok(a) => HttpResponse::Created().json(a),
Err(err) => { Err(err) => {
error!("{}", err); error!("{}", err);
@@ -125,9 +125,9 @@ async fn create(airport: web::Json<Airport>) -> HttpResponse {
} }
} }
#[put("/airports/{id}")] #[put("/airports/{icao}")]
async fn update(id: web::Path<i32>, airport: web::Json<Airport>) -> HttpResponse { async fn update(icao: web::Path<i32>, airport: web::Json<InsertAirport>) -> HttpResponse {
match Airports::update(id.into_inner(), airport.into_inner()) { match QueryAirport::update(icao.into_inner(), airport.into_inner()) {
Ok(a) => HttpResponse::Ok().json(a), Ok(a) => HttpResponse::Ok().json(a),
Err(err) => { Err(err) => {
error!("{}", err); error!("{}", err);
@@ -136,9 +136,9 @@ async fn update(id: web::Path<i32>, airport: web::Json<Airport>) -> HttpResponse
} }
} }
#[delete("/airports/{id}")] #[delete("/airports/{icao}")]
async fn delete(id: web::Path<i32>) -> HttpResponse { async fn delete(icao: web::Path<i32>) -> HttpResponse {
match Airports::delete(id.into_inner()) { match QueryAirport::delete(icao.into_inner()) {
Ok(_) => HttpResponse::NoContent().finish(), Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => { Err(err) => {
error!("{}", err); error!("{}", err);

View File

View File

@@ -1,4 +1,4 @@
use crate::{error_handler::CustomError, airports::{Airport, Airports}}; use crate::{error_handler::ServiceError, airports::{InsertAirport, QueryAirport}};
use diesel::{r2d2::ConnectionManager, PgConnection}; use diesel::{r2d2::ConnectionManager, PgConnection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::diesel_migrations::MigrationHarness; use crate::diesel_migrations::MigrationHarness;
@@ -34,18 +34,18 @@ pub fn init() {
}; };
} }
pub fn connection() -> Result<DbConnection, CustomError> { pub fn connection() -> Result<DbConnection, ServiceError> {
POOL.get() POOL.get()
.map_err(|e| CustomError::new(500, format!("Failed getting db connection: {}", e))) .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e)))
} }
pub fn import_data() { pub fn import_data() {
let path = "airport-codes.json"; let path = "airport-codes.json";
debug!("Importing data from {}", path); debug!("Importing data from {}", path);
let contents: String = std::fs::read_to_string(path).expect("Failed to read file"); let contents: String = std::fs::read_to_string(path).expect("Failed to read file");
let airports: Vec<Airport> = serde_json::from_str(&contents).expect("JSON was not well formed."); let airports: Vec<InsertAirport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
for airport in airports { for airport in airports {
match Airports::create(airport) { match QueryAirport::create(airport) {
Ok(_) => {}, Ok(_) => {},
Err(err) => error!("Error inserting airport; {}", err) Err(err) => error!("Error inserting airport; {}", err)
}; };

View File

@@ -7,14 +7,14 @@ use serde_json::json;
use std::fmt; use std::fmt;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct CustomError { pub struct ServiceError {
pub error_status_code: u16, pub error_status_code: u16,
pub error_message: String, pub error_message: String,
} }
impl CustomError { impl ServiceError {
pub fn new(error_status_code: u16, error_message: String) -> CustomError { pub fn new(error_status_code: u16, error_message: String) -> ServiceError {
CustomError { ServiceError {
error_status_code, error_status_code,
error_message, error_message,
} }
@@ -32,25 +32,25 @@ impl CustomError {
} }
} }
impl fmt::Display for CustomError { impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.error_message.as_str()) f.write_str(self.error_message.as_str())
} }
} }
impl From<DieselError> for CustomError { impl From<DieselError> for ServiceError {
fn from(error: DieselError) -> CustomError { fn from(error: DieselError) -> ServiceError {
match error { match error {
DieselError::DatabaseError(_, err) => CustomError::new(409, err.message().to_string()), DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()),
DieselError::NotFound => { DieselError::NotFound => {
CustomError::new(404, "The airport record was not found".to_string()) ServiceError::new(404, "The record was not found".to_string())
} }
err => CustomError::new(500, format!("Unknown Diesel error: {}", err)), err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)),
} }
} }
} }
impl ResponseError for CustomError { impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
let status_code = match StatusCode::from_u16(self.error_status_code) { let status_code = match StatusCode::from_u16(self.error_status_code) {
Ok(status_code) => status_code, Ok(status_code) => status_code,

View File

@@ -1,3 +1,4 @@
extern crate actix_web;
extern crate diesel; extern crate diesel;
#[macro_use] #[macro_use]
extern crate diesel_migrations; extern crate diesel_migrations;
@@ -8,12 +9,13 @@ use dotenv::dotenv;
use env_logger::Env; use env_logger::Env;
use listenfd::ListenFd; use listenfd::ListenFd;
use log::debug; use log::debug;
use std::env;
mod airports; mod airports;
mod auth;
mod db; mod db;
mod error_handler; mod error_handler;
mod metars; mod metars;
mod users;
mod schema; mod schema;
#[actix_rt::main] #[actix_rt::main]
@@ -34,6 +36,7 @@ async fn main() -> std::io::Result<()> {
App::new() App::new()
.configure(airports::init_routes) .configure(airports::init_routes)
.configure(metars::init_routes) .configure(metars::init_routes)
.configure(users::init_routes)
.wrap(cors) .wrap(cors)
.wrap(Logger::default()) .wrap(Logger::default())
}); });
@@ -41,8 +44,8 @@ async fn main() -> std::io::Result<()> {
server = match listenfd.take_tcp_listener(0)? { server = match listenfd.take_tcp_listener(0)? {
Some(listener) => server.listen(listener)?, Some(listener) => server.listen(listener)?,
None => { None => {
let host = env::var("HOST").expect("Please set host in .env"); let host = std::env::var("HOST").expect("Please set host in .env");
let port = env::var("PORT").expect("Please set port in .env"); let port = std::env::var("PORT").expect("Please set port in .env");
debug!("Binding server to {}:{}", host, port); debug!("Binding server to {}:{}", host, port);
server.bind(format!("{}:{}", host, port))? server.bind(format!("{}:{}", host, port))?
} }

View File

@@ -1,4 +1,4 @@
use crate::{error_handler::CustomError, db}; use crate::{error_handler::ServiceError, db};
use crate::schema::metars; use crate::schema::metars;
use diesel::{prelude::*, sql_query}; use diesel::{prelude::*, sql_query};
use log::{warn, trace}; use log::{warn, trace};
@@ -15,7 +15,7 @@ pub struct QualityControlFlags {
#[derive(Serialize, Deserialize, AsChangeset, Insertable)] #[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = metars)] #[diesel(table_name = metars)]
pub struct Metar { pub struct InsertMetar {
pub raw_text: String, pub raw_text: String,
pub station_id: String, pub station_id: String,
pub observation_time: String, pub observation_time: String,
@@ -42,10 +42,10 @@ pub struct Metar {
pub elevation_m: i32 pub elevation_m: i32
} }
impl Metar { impl InsertMetar {
pub fn parse(input: String) -> Result<Vec<Self>, CustomError> { pub fn parse(input: String) -> Result<Vec<Self>, ServiceError> {
if input.is_empty() { if input.is_empty() {
return Err(CustomError::new(500, "Input is empty".to_string())) return Err(ServiceError::new(500, "Input is empty".to_string()))
} }
let mut reader = Reader::from_str(&input); let mut reader = Reader::from_str(&input);
@@ -109,7 +109,7 @@ impl Metar {
#[derive(Serialize, Deserialize, Queryable, QueryableByName)] #[derive(Serialize, Deserialize, Queryable, QueryableByName)]
#[diesel(table_name = metars)] #[diesel(table_name = metars)]
pub struct Metars { pub struct QueryMetar {
pub id: i32, pub id: i32,
pub raw_text: String, pub raw_text: String,
pub station_id: String, pub station_id: String,
@@ -137,8 +137,8 @@ pub struct Metars {
pub elevation_m: i32 pub elevation_m: i32
} }
impl Metars { impl QueryMetar {
pub async fn get_all(icaos: String) -> Result<Vec<Self>, CustomError> { pub async fn get_all(icaos: String) -> Result<Vec<Self>, ServiceError> {
if icaos.is_empty() { if icaos.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
@@ -146,12 +146,12 @@ impl Metars {
let station_query: Vec<String> = station_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect(); let station_query: Vec<String> = station_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect();
let mut conn = db::connection()?; let mut conn = db::connection()?;
let mut db_metars: Vec<Metars> = match sql_query(format!("SELECT DISTINCT ON (station_id) * FROM metars WHERE station_id IN ({}) ORDER BY station_id, observation_time DESC", station_query.join(","))).load(&mut conn) { let mut db_metars: Vec<QueryMetar> = match sql_query(format!("SELECT DISTINCT ON (station_id) * FROM metars WHERE station_id IN ({}) ORDER BY station_id, observation_time DESC", station_query.join(","))).load(&mut conn) {
Ok(m) => m, Ok(m) => m,
Err(err) => return Err(CustomError { error_status_code: 500, error_message: format!("{}", err) }) Err(err) => return Err(ServiceError { error_status_code: 500, error_message: format!("{}", err) })
}; };
fn get_missing_metar_icaos(db_metars: &Vec<Metars>, station_icaos: Vec<&str>) -> Vec<String> { fn get_missing_metar_icaos(db_metars: &Vec<QueryMetar>, station_icaos: Vec<&str>) -> Vec<String> {
let mut missing_metar_icaos: Vec<String> = vec![]; let mut missing_metar_icaos: Vec<String> = vec![];
let current_time = chrono::Local::now().naive_local().timestamp(); let current_time = chrono::Local::now().naive_local().timestamp();
let db_metars_set: HashSet<&str> = db_metars.iter().map(|icao| icao.station_id.as_str()).collect(); let db_metars_set: HashSet<&str> = db_metars.iter().map(|icao| icao.station_id.as_str()).collect();
@@ -182,10 +182,10 @@ impl Metars {
trace!("Retrieving missing METAR data for {:?}", missing_icaos); trace!("Retrieving missing METAR data for {:?}", missing_icaos);
let missing_icaos_string: Vec<String> = missing_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect(); let missing_icaos_string: Vec<String> = missing_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect();
let url = format!("https://beta.aviationweather.gov/cgi-bin/data/metar.php?ids={}&format=xml", missing_icaos_string.join(",")); let url = format!("https://beta.aviationweather.gov/cgi-bin/data/metar.php?ids={}&format=xml", missing_icaos_string.join(","));
let metars: Vec<Metar> = match reqwest::get(url).await { let metars: Vec<InsertMetar> = match reqwest::get(url).await {
Ok(r) => match r.text().await { Ok(r) => match r.text().await {
Ok(r) => { Ok(r) => {
match Metar::parse(r) { match InsertMetar::parse(r) {
Ok(m) => m, Ok(m) => m,
Err(err) => { Err(err) => {
warn!("{}", err); warn!("{}", err);

View File

@@ -1,18 +1,18 @@
use crate::{error_handler::CustomError, db::Metadata}; use crate::{error_handler::ServiceError, db::Metadata};
use crate::metars::Metars; use crate::metars::QueryMetar;
use actix_web::{get, web, HttpResponse, Responder}; use actix_web::{get, web, HttpResponse, Responder};
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct MetarsResponse { pub struct MetarsResponse {
pub data: Vec<Metars>, pub data: Vec<QueryMetar>,
pub meta: Metadata pub meta: Metadata
} }
#[get("metars/{ids}")] #[get("metars/{ids}")]
async fn get_all(ids: web::Path<String>) -> impl Responder { async fn get_all(ids: web::Path<String>) -> impl Responder {
let airports = match web::block(|| Ok::<_, CustomError>(async {Metars::get_all(ids.into_inner()).await})) let airports = match web::block(|| Ok::<_, ServiceError>(async {QueryMetar::get_all(ids.into_inner()).await}))
.await .await
.unwrap() .unwrap()
.unwrap() .unwrap()

View File

@@ -1,6 +1,6 @@
diesel::table! { diesel::table! {
use postgis_diesel::sql_types::*;
use diesel::sql_types::*; use diesel::sql_types::*;
use postgis_diesel::sql_types::*;
airports (icao) { airports (icao) {
icao -> Text, icao -> Text,
id -> Integer, id -> Integer,
@@ -43,3 +43,15 @@ diesel::table! {
elevation_m -> Integer, elevation_m -> Integer,
} }
} }
diesel::table! {
use diesel::sql_types::*;
use crate::users::PgUserType;
users (id) {
id -> Uuid,
first_name -> Text,
last_name -> Text,
user_type -> PgUserType,
favorites -> Array<Text>
}
}

View File

@@ -0,0 +1,7 @@
mod model;
mod routes;
mod user_type;
pub use user_type::PgUserType;
pub use model::*;
pub use routes::init_routes;

View File

@@ -0,0 +1,50 @@
use std::{future::Future, pin::Pin, sync::RwLock};
use actix_web::{dev::Payload, error::ErrorUnauthorized, web, Error, FromRequest, HttpRequest};
use actix_identity::Identity;
use diesel::{query_builder::AsChangeset, prelude::Insertable};
use log::warn;
use crate::schema::users;
use serde::{Serialize, Deserialize};
use super::user_type::UserType;
#[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = users)]
pub struct InsertUser {
first_name: String,
last_name: String,
user_type: UserType,
favorites: Vec<String>
}
// impl FromRequest for InsertUser {
// type Config = ();
// type Error = Error;
// type Future = Pin<Box<dyn Future<Output = Result<Self, Error>>>>;
// fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
// let fut = Identity::from_request(req, pl);
// let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
// if sessions.is_none() {
// warn!("sessions is empty(none)!");
// return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
// }
// let sessions = sessions.unwrap().clone();
// Box::pin(async move {
// if let Some(identity) = fut.await?.identity() {
// if let Some(user) = sessions
// .read()
// .unwrap()
// .map
// .get(&identity)
// .map(|x| x.clone())
// {
// return Ok(user);
// }
// };
// Err(ErrorUnauthorized("unauthorized"))
// })
// }
// }

View File

@@ -0,0 +1,51 @@
use actix_web::{get, post, delete, put, web, HttpResponse};
#[get("users")]
async fn get() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[get("users/{id}")]
async fn get_all() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[post("users")]
async fn create() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[delete("users")]
async fn delete() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[put("users")]
async fn update() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[get("users/favorites")]
async fn get_favorites() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[post("users/favorites")]
async fn add_favorite() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[delete("users/favorites")]
async fn delete_favorite() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(get);
config.service(create);
config.service(delete);
config.service(update);
config.service(get_favorites);
config.service(add_favorite);
config.service(delete_favorite);
}

View File

@@ -0,0 +1,35 @@
use std::io::Write;
use diesel::{sql_types::SqlType, deserialize::{FromSqlRow, FromSql, self}, expression::AsExpression, serialize::{ToSql, Output, self, IsNull}, pg::{Pg, PgValue}};
use serde::{Serialize, Deserialize};
#[derive(SqlType)]
#[diesel(postgres_type(name = "User_Type"))]
pub struct PgUserType;
#[derive(Serialize, Deserialize, Debug, PartialEq, FromSqlRow, AsExpression, Eq)]
#[diesel(sql_type = PgUserType)]
pub enum UserType {
Admin,
User,
}
impl ToSql<PgUserType, Pg> for UserType {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
match *self {
Self::Admin => out.write_all(b"admin")?,
Self::User => out.write_all(b"user")?,
}
Ok(IsNull::No)
}
}
impl FromSql<PgUserType, Pg> for UserType {
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
match bytes.as_bytes() {
b"admin" => Ok(Self::Admin),
b"user" => Ok(Self::User),
_ => Err("Unrecognized enum variant".into()),
}
}
}

View File

@@ -1,5 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {}
} }

View File

@@ -118,7 +118,15 @@ export default function MapTiles() {
position={[airport.point.y, airport.point.x]} position={[airport.point.y, airport.point.x]}
icon={icon(airport)} icon={icon(airport)}
eventHandlers={{ eventHandlers={{
click: () => handleOpen(airport) click: () => {
mapEvents.eachLayer((l) => {
if (l.getTooltip() && l.isTooltipOpen()) {
console.log('l', l);
l.closeTooltip();
}
});
handleOpen(airport);
}
}} }}
> >
{!isOpen && ( {!isOpen && (

View File

@@ -36,13 +36,17 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
} }
function windColor(metar: Metar | undefined) { function windColor(metar: Metar | undefined) {
if (Number(metar?.wind_speed_kt) <= 9) { if (metar) {
if (Number(metar.wind_speed_kt) <= 9) {
return 'bg-green-300'; return 'bg-green-300';
} else if (Number(metar?.wind_speed_kt) > 9) { } else if (Number(metar.wind_speed_kt) <= 12) {
return 'bg-orange-300'; return 'bg-orange-300';
} else if (Number(metar?.wind_speed_kt) > 12) { } else {
return 'bg-red-300'; return 'bg-red-300';
} }
} else {
return 'gb-gray-100';
}
} }
return ( return (
<Modal <Modal

View File

@@ -9,8 +9,8 @@ export default function Map({ className = '' }: { className?: string }) {
<MapContainer <MapContainer
center={[38.7209, -77.5133]} center={[38.7209, -77.5133]}
zoom={8} zoom={8}
maxZoom={12} maxZoom={14} // Zoomed in
minZoom={1} minZoom={3} // Zoomed out
id='map-container' id='map-container'
style={{ height: '94.5vh' }} style={{ height: '94.5vh' }}
className={`${className} overflow-y-hidden overflow-x-hidden`} className={`${className} overflow-y-hidden overflow-x-hidden`}