From 4a3225c3f96c29334da82043370b38085669e54a Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Sat, 16 Sep 2023 22:39:02 -0400 Subject: [PATCH] updating api formats --- weather-service/Cargo.lock | 206 +++++++++++++++++- weather-service/Cargo.toml | 5 +- weather-service/Makefile | 2 +- weather-service/README.md | 56 +++++ .../migrations/000002_create_users/down.sql | 1 + .../migrations/000002_create_users/up.sql | 11 + weather-service/src/airports/model.rs | 30 +-- weather-service/src/airports/routes.rs | 26 +-- weather-service/src/auth/mod.rs | 0 weather-service/src/db.rs | 10 +- weather-service/src/error_handler.rs | 22 +- weather-service/src/main.rs | 9 +- weather-service/src/metars/model.rs | 26 +-- weather-service/src/metars/routes.rs | 8 +- weather-service/src/schema.rs | 14 +- weather-service/src/users/mod.rs | 7 + weather-service/src/users/model.rs | 50 +++++ weather-service/src/users/routes.rs | 51 +++++ weather-service/src/users/user_type.rs | 35 +++ weather-ui/postcss.config.js | 2 + .../src/app/_components/Metars/MapTiles.tsx | 10 +- .../app/_components/Metars/MetarDialog.tsx | 16 +- .../src/app/_components/Metars/MetarMap.tsx | 4 +- 23 files changed, 522 insertions(+), 79 deletions(-) create mode 100644 weather-service/README.md create mode 100644 weather-service/migrations/000002_create_users/down.sql create mode 100644 weather-service/migrations/000002_create_users/up.sql create mode 100644 weather-service/src/auth/mod.rs create mode 100644 weather-service/src/users/mod.rs create mode 100644 weather-service/src/users/model.rs create mode 100644 weather-service/src/users/routes.rs create mode 100644 weather-service/src/users/user_type.rs diff --git a/weather-service/Cargo.lock b/weather-service/Cargo.lock index 4032ca2..cff631a 100644 --- a/weather-service/Cargo.lock +++ b/weather-service/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.4", "bitflags 2.4.0", "brotli", "bytes", @@ -73,6 +73,22 @@ dependencies = [ "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]] name = "actix-macros" version = "0.2.4" @@ -135,6 +151,23 @@ dependencies = [ "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]] name = "actix-utils" version = "3.0.1" @@ -212,6 +245,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "ahash" version = "0.8.3" @@ -263,6 +331,23 @@ dependencies = [ "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]] name = "autocfg" version = "1.1.0" @@ -284,6 +369,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.4" @@ -390,6 +481,16 @@ dependencies = [ "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]] name = "convert_case" version = "0.4.0" @@ -402,7 +503,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] @@ -448,9 +556,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "deranged" version = "0.3.8" @@ -526,6 +644,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -689,6 +808,16 @@ dependencies = [ "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]] name = "gimli" version = "0.28.0" @@ -732,6 +861,24 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "http" version = "0.2.9" @@ -862,6 +1009,15 @@ dependencies = [ "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]] name = "ipnet" version = "2.8.0" @@ -1068,6 +1224,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.57" @@ -1165,6 +1327,18 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "postgis_diesel" version = "2.2.1" @@ -1304,7 +1478,7 @@ version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -1485,6 +1659,17 @@ dependencies = [ "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]] name = "signal-hook-registry" version = "1.4.1" @@ -1529,6 +1714,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -1763,6 +1954,16 @@ dependencies = [ "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]] name = "url" version = "2.4.1" @@ -1882,6 +2083,7 @@ name = "weather-service" version = "0.1.0" dependencies = [ "actix-cors", + "actix-identity", "actix-rt", "actix-web", "chrono", diff --git a/weather-service/Cargo.toml b/weather-service/Cargo.toml index f30d446..3e1db39 100644 --- a/weather-service/Cargo.toml +++ b/weather-service/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] actix-web = "4.4.0" actix-rt = "2.9.0" +actix-cors = "0.6.4" +actix-identity = "0.5.0" chrono = { version = "0.4.30", features = ["serde"] } dotenv = "0.15.0" diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] } @@ -23,5 +25,4 @@ serde = {version = "1.0.188", features = ["derive"]} serde_json = "1.0.105" tokio = { version = "1.32.0", features = ["macros", "rt"] } uuid = { version = "1.4.1", features = ["serde", "v4"] } -log = "0.4.20" -actix-cors = "0.6.4" +log = "0.4.20" \ No newline at end of file diff --git a/weather-service/Makefile b/weather-service/Makefile index 5a6a0d4..d6c6e35 100644 --- a/weather-service/Makefile +++ b/weather-service/Makefile @@ -27,7 +27,7 @@ clean: rm -rf target 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 \ No newline at end of file diff --git a/weather-service/README.md b/weather-service/README.md new file mode 100644 index 0000000..1ec6484 --- /dev/null +++ b/weather-service/README.md @@ -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 +``` +``` diff --git a/weather-service/migrations/000002_create_users/down.sql b/weather-service/migrations/000002_create_users/down.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/weather-service/migrations/000002_create_users/down.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/weather-service/migrations/000002_create_users/up.sql b/weather-service/migrations/000002_create_users/up.sql new file mode 100644 index 0000000..b7f0206 --- /dev/null +++ b/weather-service/migrations/000002_create_users/up.sql @@ -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[] +); \ No newline at end of file diff --git a/weather-service/src/airports/model.rs b/weather-service/src/airports/model.rs index 42de092..af3fd54 100644 --- a/weather-service/src/airports/model.rs +++ b/weather-service/src/airports/model.rs @@ -1,15 +1,15 @@ use crate::db; -use crate::error_handler::CustomError; +use crate::error_handler::ServiceError; use crate::schema::airports; use diesel::prelude::*; -use log::trace; +// use log::trace; use postgis_diesel::types::*; use postgis_diesel::functions::*; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, AsChangeset, Insertable)] #[diesel(table_name = airports)] -pub struct Airport { +pub struct InsertAirport { pub icao: String, pub category: String, pub full_name: String, @@ -26,7 +26,7 @@ pub struct Airport { #[derive(Serialize, Deserialize, Queryable, QueryableByName)] #[diesel(table_name = airports)] -pub struct Airports { +pub struct QueryAirport { pub icao: String, pub id: i32, pub category: String, @@ -42,13 +42,13 @@ pub struct Airports { pub point: Point } -impl Airports { - pub fn get_all(bounds: Option>, category: Option, filter: Option, limit: i32, page: i32) -> Result, CustomError> { +impl QueryAirport { + pub fn get_all(bounds: Option>, category: Option, filter: Option, limit: i32, page: i32) -> Result, ServiceError> { let mut conn = db::connection()?; let mut query = airports::table .limit(limit as i64) .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 { query = query.filter(st_contains(bounds, airports::point)); @@ -62,28 +62,28 @@ impl Airports { .or(airports::full_name.ilike(format!("%{}%", filter))) ) } - let debug = diesel::debug_query::(&query); - trace!("{}", debug); - let airports: Vec = query.order(airports::category.asc()).load::(&mut conn)?; + // let debug = diesel::debug_query::(&query); + // trace!("{}", debug); + let airports: Vec = query.order(airports::category.asc()).load::(&mut conn)?; Ok(airports) } - pub fn find(icao: String) -> Result { + pub fn find(icao: String) -> Result { let mut conn = db::connection()?; let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?; Ok(airport) } -pub fn create(airport: Airport) -> Result { +pub fn create(airport: InsertAirport) -> Result { let mut conn = db::connection()?; - let airport = Airport::from(airport); + let airport = InsertAirport::from(airport); let airport = diesel::insert_into(airports::table) .values(airport) .get_result(&mut conn)?; Ok(airport) } -pub fn update(id: i32, airport: Airport) -> Result { +pub fn update(id: i32, airport: InsertAirport) -> Result { let mut conn = db::connection()?; let airport = diesel::update(airports::table) .filter(airports::id.eq(id)) @@ -92,7 +92,7 @@ pub fn update(id: i32, airport: Airport) -> Result { Ok(airport) } -pub fn delete(id: i32) -> Result { +pub fn delete(id: i32) -> Result { let mut conn = db::connection()?; let res = diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?; Ok(res) diff --git a/weather-service/src/airports/routes.rs b/weather-service/src/airports/routes.rs index 2651075..067636b 100644 --- a/weather-service/src/airports/routes.rs +++ b/weather-service/src/airports/routes.rs @@ -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 log::{error, warn}; use postgis_diesel::types::{Polygon, Point}; @@ -21,7 +21,7 @@ async fn import() -> HttpResponse { #[derive(Serialize, Deserialize)] pub struct AirportsResponse { - pub data: Vec, + pub data: Vec, pub meta: Metadata } @@ -82,7 +82,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { 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 { data: a, meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } @@ -96,13 +96,13 @@ async fn get_all(req: HttpRequest) -> HttpResponse { #[derive(Serialize, Deserialize)] pub struct AirportResponse { - pub data: Airports, + pub data: QueryAirport, pub meta: Metadata } #[get("/airports/{icao}")] async fn get(icao: web::Path) -> HttpResponse { - match Airports::find(icao.into_inner()) { + match QueryAirport::find(icao.into_inner()) { Ok(a) => HttpResponse::Ok().json(AirportResponse { data: a, meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } @@ -115,8 +115,8 @@ async fn get(icao: web::Path) -> HttpResponse { } #[post("/airports")] -async fn create(airport: web::Json) -> HttpResponse { - match Airports::create(airport.into_inner()) { +async fn create(airport: web::Json) -> HttpResponse { + match QueryAirport::create(airport.into_inner()) { Ok(a) => HttpResponse::Created().json(a), Err(err) => { error!("{}", err); @@ -125,9 +125,9 @@ async fn create(airport: web::Json) -> HttpResponse { } } -#[put("/airports/{id}")] -async fn update(id: web::Path, airport: web::Json) -> HttpResponse { - match Airports::update(id.into_inner(), airport.into_inner()) { +#[put("/airports/{icao}")] +async fn update(icao: web::Path, airport: web::Json) -> HttpResponse { + match QueryAirport::update(icao.into_inner(), airport.into_inner()) { Ok(a) => HttpResponse::Ok().json(a), Err(err) => { error!("{}", err); @@ -136,9 +136,9 @@ async fn update(id: web::Path, airport: web::Json) -> HttpResponse } } -#[delete("/airports/{id}")] -async fn delete(id: web::Path) -> HttpResponse { - match Airports::delete(id.into_inner()) { +#[delete("/airports/{icao}")] +async fn delete(icao: web::Path) -> HttpResponse { + match QueryAirport::delete(icao.into_inner()) { Ok(_) => HttpResponse::NoContent().finish(), Err(err) => { error!("{}", err); diff --git a/weather-service/src/auth/mod.rs b/weather-service/src/auth/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/weather-service/src/db.rs b/weather-service/src/db.rs index 666549c..0f31144 100644 --- a/weather-service/src/db.rs +++ b/weather-service/src/db.rs @@ -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 serde::{Deserialize, Serialize}; use crate::diesel_migrations::MigrationHarness; @@ -34,18 +34,18 @@ pub fn init() { }; } -pub fn connection() -> Result { +pub fn connection() -> Result { 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() { let path = "airport-codes.json"; debug!("Importing data from {}", path); let contents: String = std::fs::read_to_string(path).expect("Failed to read file"); - let airports: Vec = serde_json::from_str(&contents).expect("JSON was not well formed."); + let airports: Vec = serde_json::from_str(&contents).expect("JSON was not well formed."); for airport in airports { - match Airports::create(airport) { + match QueryAirport::create(airport) { Ok(_) => {}, Err(err) => error!("Error inserting airport; {}", err) }; diff --git a/weather-service/src/error_handler.rs b/weather-service/src/error_handler.rs index 4025800..5aa4a46 100644 --- a/weather-service/src/error_handler.rs +++ b/weather-service/src/error_handler.rs @@ -7,14 +7,14 @@ use serde_json::json; use std::fmt; #[derive(Debug, Deserialize, Serialize)] -pub struct CustomError { +pub struct ServiceError { pub error_status_code: u16, pub error_message: String, } -impl CustomError { - pub fn new(error_status_code: u16, error_message: String) -> CustomError { - CustomError { +impl ServiceError { + pub fn new(error_status_code: u16, error_message: String) -> ServiceError { + ServiceError { error_status_code, 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 { f.write_str(self.error_message.as_str()) } } -impl From for CustomError { - fn from(error: DieselError) -> CustomError { +impl From for ServiceError { + fn from(error: DieselError) -> ServiceError { match error { - DieselError::DatabaseError(_, err) => CustomError::new(409, err.message().to_string()), + DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()), 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 { let status_code = match StatusCode::from_u16(self.error_status_code) { Ok(status_code) => status_code, diff --git a/weather-service/src/main.rs b/weather-service/src/main.rs index 3d1c06e..58ef191 100644 --- a/weather-service/src/main.rs +++ b/weather-service/src/main.rs @@ -1,3 +1,4 @@ +extern crate actix_web; extern crate diesel; #[macro_use] extern crate diesel_migrations; @@ -8,12 +9,13 @@ use dotenv::dotenv; use env_logger::Env; use listenfd::ListenFd; use log::debug; -use std::env; mod airports; +mod auth; mod db; mod error_handler; mod metars; +mod users; mod schema; #[actix_rt::main] @@ -34,6 +36,7 @@ async fn main() -> std::io::Result<()> { App::new() .configure(airports::init_routes) .configure(metars::init_routes) + .configure(users::init_routes) .wrap(cors) .wrap(Logger::default()) }); @@ -41,8 +44,8 @@ async fn main() -> std::io::Result<()> { server = match listenfd.take_tcp_listener(0)? { Some(listener) => server.listen(listener)?, None => { - let host = env::var("HOST").expect("Please set host in .env"); - let port = env::var("PORT").expect("Please set port in .env"); + let host = std::env::var("HOST").expect("Please set host in .env"); + let port = std::env::var("PORT").expect("Please set port in .env"); debug!("Binding server to {}:{}", host, port); server.bind(format!("{}:{}", host, port))? } diff --git a/weather-service/src/metars/model.rs b/weather-service/src/metars/model.rs index 62ddefc..78a899a 100644 --- a/weather-service/src/metars/model.rs +++ b/weather-service/src/metars/model.rs @@ -1,4 +1,4 @@ -use crate::{error_handler::CustomError, db}; +use crate::{error_handler::ServiceError, db}; use crate::schema::metars; use diesel::{prelude::*, sql_query}; use log::{warn, trace}; @@ -15,7 +15,7 @@ pub struct QualityControlFlags { #[derive(Serialize, Deserialize, AsChangeset, Insertable)] #[diesel(table_name = metars)] -pub struct Metar { +pub struct InsertMetar { pub raw_text: String, pub station_id: String, pub observation_time: String, @@ -42,10 +42,10 @@ pub struct Metar { pub elevation_m: i32 } -impl Metar { - pub fn parse(input: String) -> Result, CustomError> { +impl InsertMetar { + pub fn parse(input: String) -> Result, ServiceError> { 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); @@ -109,7 +109,7 @@ impl Metar { #[derive(Serialize, Deserialize, Queryable, QueryableByName)] #[diesel(table_name = metars)] -pub struct Metars { +pub struct QueryMetar { pub id: i32, pub raw_text: String, pub station_id: String, @@ -137,8 +137,8 @@ pub struct Metars { pub elevation_m: i32 } -impl Metars { - pub async fn get_all(icaos: String) -> Result, CustomError> { +impl QueryMetar { + pub async fn get_all(icaos: String) -> Result, ServiceError> { if icaos.is_empty() { return Ok(vec![]); } @@ -146,12 +146,12 @@ impl Metars { let station_query: Vec = station_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect(); let mut conn = db::connection()?; - let mut db_metars: Vec = 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 = 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, - 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, station_icaos: Vec<&str>) -> Vec { + fn get_missing_metar_icaos(db_metars: &Vec, station_icaos: Vec<&str>) -> Vec { let mut missing_metar_icaos: Vec = vec![]; 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(); @@ -182,10 +182,10 @@ impl Metars { trace!("Retrieving missing METAR data for {:?}", missing_icaos); let missing_icaos_string: Vec = 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 metars: Vec = match reqwest::get(url).await { + let metars: Vec = match reqwest::get(url).await { Ok(r) => match r.text().await { Ok(r) => { - match Metar::parse(r) { + match InsertMetar::parse(r) { Ok(m) => m, Err(err) => { warn!("{}", err); diff --git a/weather-service/src/metars/routes.rs b/weather-service/src/metars/routes.rs index b9a8f71..7dfe3ad 100644 --- a/weather-service/src/metars/routes.rs +++ b/weather-service/src/metars/routes.rs @@ -1,18 +1,18 @@ -use crate::{error_handler::CustomError, db::Metadata}; -use crate::metars::Metars; +use crate::{error_handler::ServiceError, db::Metadata}; +use crate::metars::QueryMetar; use actix_web::{get, web, HttpResponse, Responder}; use log::error; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct MetarsResponse { - pub data: Vec, + pub data: Vec, pub meta: Metadata } #[get("metars/{ids}")] async fn get_all(ids: web::Path) -> 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 .unwrap() .unwrap() diff --git a/weather-service/src/schema.rs b/weather-service/src/schema.rs index 5b0bc31..22e53dd 100644 --- a/weather-service/src/schema.rs +++ b/weather-service/src/schema.rs @@ -1,6 +1,6 @@ diesel::table! { - use postgis_diesel::sql_types::*; use diesel::sql_types::*; + use postgis_diesel::sql_types::*; airports (icao) { icao -> Text, id -> Integer, @@ -43,3 +43,15 @@ diesel::table! { 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 + } +} diff --git a/weather-service/src/users/mod.rs b/weather-service/src/users/mod.rs new file mode 100644 index 0000000..6570399 --- /dev/null +++ b/weather-service/src/users/mod.rs @@ -0,0 +1,7 @@ +mod model; +mod routes; +mod user_type; + +pub use user_type::PgUserType; +pub use model::*; +pub use routes::init_routes; \ No newline at end of file diff --git a/weather-service/src/users/model.rs b/weather-service/src/users/model.rs new file mode 100644 index 0000000..75ce23c --- /dev/null +++ b/weather-service/src/users/model.rs @@ -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 +} + +// impl FromRequest for InsertUser { +// type Config = (); +// type Error = Error; +// type Future = Pin>>>; + +// fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future { +// let fut = Identity::from_request(req, pl); +// let sessions: Option<&web::Data>> = 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")) +// }) +// } +// } \ No newline at end of file diff --git a/weather-service/src/users/routes.rs b/weather-service/src/users/routes.rs new file mode 100644 index 0000000..60979c5 --- /dev/null +++ b/weather-service/src/users/routes.rs @@ -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); +} \ No newline at end of file diff --git a/weather-service/src/users/user_type.rs b/weather-service/src/users/user_type.rs new file mode 100644 index 0000000..82166ec --- /dev/null +++ b/weather-service/src/users/user_type.rs @@ -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 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 for UserType { + fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { + match bytes.as_bytes() { + b"admin" => Ok(Self::Admin), + b"user" => Ok(Self::User), + _ => Err("Unrecognized enum variant".into()), + } + } +} \ No newline at end of file diff --git a/weather-ui/postcss.config.js b/weather-ui/postcss.config.js index 5cbc2c7..b063a5f 100644 --- a/weather-ui/postcss.config.js +++ b/weather-ui/postcss.config.js @@ -1,5 +1,7 @@ module.exports = { plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, tailwindcss: {}, autoprefixer: {} } diff --git a/weather-ui/src/app/_components/Metars/MapTiles.tsx b/weather-ui/src/app/_components/Metars/MapTiles.tsx index f0c8a48..4b3c958 100644 --- a/weather-ui/src/app/_components/Metars/MapTiles.tsx +++ b/weather-ui/src/app/_components/Metars/MapTiles.tsx @@ -118,7 +118,15 @@ export default function MapTiles() { position={[airport.point.y, airport.point.x]} icon={icon(airport)} eventHandlers={{ - click: () => handleOpen(airport) + click: () => { + mapEvents.eachLayer((l) => { + if (l.getTooltip() && l.isTooltipOpen()) { + console.log('l', l); + l.closeTooltip(); + } + }); + handleOpen(airport); + } }} > {!isOpen && ( diff --git a/weather-ui/src/app/_components/Metars/MetarDialog.tsx b/weather-ui/src/app/_components/Metars/MetarDialog.tsx index 083896d..5876fdf 100644 --- a/weather-ui/src/app/_components/Metars/MetarDialog.tsx +++ b/weather-ui/src/app/_components/Metars/MetarDialog.tsx @@ -36,12 +36,16 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro } function windColor(metar: Metar | undefined) { - if (Number(metar?.wind_speed_kt) <= 9) { - return 'bg-green-300'; - } else if (Number(metar?.wind_speed_kt) > 9) { - return 'bg-orange-300'; - } else if (Number(metar?.wind_speed_kt) > 12) { - return 'bg-red-300'; + if (metar) { + if (Number(metar.wind_speed_kt) <= 9) { + return 'bg-green-300'; + } else if (Number(metar.wind_speed_kt) <= 12) { + return 'bg-orange-300'; + } else { + return 'bg-red-300'; + } + } else { + return 'gb-gray-100'; } } return ( diff --git a/weather-ui/src/app/_components/Metars/MetarMap.tsx b/weather-ui/src/app/_components/Metars/MetarMap.tsx index 4644a74..fb5310c 100644 --- a/weather-ui/src/app/_components/Metars/MetarMap.tsx +++ b/weather-ui/src/app/_components/Metars/MetarMap.tsx @@ -9,8 +9,8 @@ export default function Map({ className = '' }: { className?: string }) {