diff --git a/service/Cargo.lock b/service/Cargo.lock index 8afaddb..67d4453 100644 --- a/service/Cargo.lock +++ b/service/Cargo.lock @@ -44,8 +44,8 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash", - "base64", + "ahash 0.8.3", + "base64 0.21.4", "bitflags 2.4.0", "brotli", "bytes", @@ -197,7 +197,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", + "ahash 0.8.3", "bytes", "bytestring", "cfg-if", @@ -242,7 +242,7 @@ checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" dependencies = [ "actix-utils", "actix-web", - "base64", + "base64 0.21.4", "futures-core", "futures-util", "log", @@ -264,6 +264,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.3" @@ -344,12 +355,52 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "attohttpc" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" +dependencies = [ + "http", + "log", + "native-tls", + "serde", + "serde_json", + "url", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "aws-creds" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3776743bb68d4ad02ba30ba8f64373f1be4e082fe47651767171ce75bb2f6cf5" +dependencies = [ + "attohttpc", + "dirs", + "log", + "quick-xml", + "rust-ini", + "serde", + "thiserror", + "time", + "url", +] + +[[package]] +name = "aws-region" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42fed2b9fca70f2908268d057a607f2a906f47edbf856ea8587de9038d264e22" +dependencies = [ + "thiserror", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -365,6 +416,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.4" @@ -601,6 +658,9 @@ name = "deranged" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] [[package]] name = "derive_more" @@ -675,6 +735,32 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dotenv" version = "0.15.0" @@ -916,6 +1002,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] [[package]] name = "hashbrown" @@ -929,6 +1018,21 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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" @@ -1112,7 +1216,7 @@ version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" dependencies = [ - "base64", + "base64 0.21.4", "pem", "ring", "serde", @@ -1138,6 +1242,17 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.0", + "libc", + "redox_syscall 0.4.1", +] + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -1178,6 +1293,23 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "maybe-async" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.6.3" @@ -1211,6 +1343,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minidom" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278" +dependencies = [ + "rxml", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1339,6 +1480,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1357,7 +1508,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] @@ -1391,7 +1542,7 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" dependencies = [ - "base64", + "base64 0.21.4", "serde", ] @@ -1474,6 +1625,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.33" @@ -1558,6 +1719,26 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -1593,7 +1774,7 @@ version = "0.11.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78fdbab6a7e1d7b13cc8ff10197f47986b41c639300cc3c8158cac7847c9bbef" dependencies = [ - "base64", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -1617,10 +1798,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -1639,6 +1822,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust-s3" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2ac5ff6acfbe74226fa701b5ef793aaa054055c13ebb7060ad36942956e027" +dependencies = [ + "async-trait", + "aws-creds", + "aws-region", + "base64 0.13.1", + "bytes", + "cfg-if", + "futures", + "hex", + "hmac", + "http", + "log", + "maybe-async", + "md5", + "minidom", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_derive", + "sha2", + "thiserror", + "time", + "tokio", + "tokio-stream", + "url", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1667,6 +1893,23 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rxml" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7" +dependencies = [ + "bytes", + "rxml_validation", + "smartstring", +] + +[[package]] +name = "rxml_validation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530" + [[package]] name = "ryu" version = "1.0.15" @@ -1810,6 +2053,7 @@ dependencies = [ "redis", "regex", "reqwest", + "rust-s3", "rustix", "serde", "serde_json", @@ -1834,6 +2078,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1870,6 +2125,17 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.4.9" @@ -1896,6 +2162,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -1959,7 +2231,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] @@ -2086,6 +2358,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -2314,6 +2597,19 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" diff --git a/service/Cargo.toml b/service/Cargo.toml index 057c305..7cb8451 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -34,3 +34,4 @@ redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r rustix = "0.38.19" # https://github.com/imsnif/bandwhich/issues/284 regex = "1.10.2" futures-util = "0.3.29" +rust-s3 = "0.33.0" diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs index c1f8eb2..7ccd927 100644 --- a/service/src/db/mod.rs +++ b/service/src/db/mod.rs @@ -1,10 +1,11 @@ use crate::error_handler::ServiceError; use diesel::{r2d2::ConnectionManager, PgConnection}; use redis::{Client as RedisClient, aio::Connection as RedisConnection}; +use s3::{Bucket, Region, creds::Credentials, BucketConfiguration, request::ResponseData, bucket_ops::CreateBucketResponse}; use serde::{Deserialize, Serialize}; use crate::diesel_migrations::MigrationHarness; use lazy_static::lazy_static; -use log::{error, info}; +use log::{error, info, warn}; use r2d2; use std::env; @@ -32,11 +33,43 @@ lazy_static! { let url = format!("redis://{}:{}", host, port); RedisClient::open(url).expect("Failed to create redis client") }; + static ref BUCKET: Bucket = { + let url = env::var("MINIO_HOST").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + let base_url = format!("http://{}:{}", url, port); + + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + + Bucket::new("aviation", region.clone(), credentials.clone()).expect("Failed to create S3 Bucket").with_path_style() + }; } -pub fn init() { +pub async fn init() { lazy_static::initialize(&POOL); lazy_static::initialize(&REDIS); + lazy_static::initialize(&BUCKET); + match create_bucket().await { + Ok(_) => info!("Bucket initialized"), + Err(err) => { + match err.status { + 409 => warn!("Bucket already exists"), + _ => error!("Failed to initialize bucket; {}", err.message) + } + } + }; let mut pool: DbConnection = connection().expect("Failed to get db connection"); match pool.run_pending_migrations(MIGRATIONS) { Ok(_) => info!("Database initialized"), @@ -59,6 +92,46 @@ pub async fn redis_async_connection() -> Result { Ok(conn) } +async fn create_bucket() -> Result { + let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + let base_url = format!("http://{}:{}", url, port); + + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + let bucket_name = "aviation"; + let response = Bucket::create_with_path_style(bucket_name, region, credentials, BucketConfiguration::default()).await?; + Ok(response) +} + +pub async fn upload_file(path: &str, content: &[u8]) -> Result { + let response = BUCKET.put_object(path, content).await?; + Ok(response) +} + +pub async fn get_file(path: &str) -> Result, ServiceError> { + let response = BUCKET.get_object(path).await?; + let bytes = response.bytes(); + Ok(bytes.to_vec()) +} + +pub async fn delete_file(path: &str) -> Result { + let response = BUCKET.delete_object(path).await?; + Ok(response) +} + #[derive(Serialize, Deserialize)] pub struct Response { pub data: T, diff --git a/service/src/error_handler.rs b/service/src/error_handler.rs index aae201c..726fb72 100644 --- a/service/src/error_handler.rs +++ b/service/src/error_handler.rs @@ -102,6 +102,20 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: s3::error::S3Error) -> ServiceError { + match error { + s3::error::S3Error::Credentials(err) => ServiceError::new(500, format!("Unknown s3 credentials error: {}", err)), + s3::error::S3Error::FromUtf8(err) => ServiceError::new(500, format!("Unknown s3 from utf8 error: {}", err)), + s3::error::S3Error::FmtError(err) => ServiceError::new(500, format!("Unknown s3 fmt error: {}", err)), + s3::error::S3Error::HeaderToStr(err) => ServiceError::new(500, format!("Unknown s3 header to str error: {}", err)), + s3::error::S3Error::HmacInvalidLength(err) => ServiceError::new(500, format!("Unknown s3 hmac invalid length error: {}", err)), + s3::error::S3Error::Http(status, msg) => ServiceError::new(status, msg), + _ => ServiceError::new(500, format!("Unknown s3 error: {}", error)) + } + } +} + impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status = match StatusCode::from_u16(self.status) { diff --git a/service/src/main.rs b/service/src/main.rs index c1a8500..f8ba3d0 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -21,7 +21,7 @@ mod scheduler; async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,service=info")); - db::init(); + db::init().await; // scheduler::update_airports(); let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs index 512c93a..5133761 100644 --- a/service/src/users/routes.rs +++ b/service/src/users/routes.rs @@ -1,8 +1,10 @@ +use actix_multipart::Multipart; use actix_web::{get, post, delete, web, HttpResponse, ResponseError}; +use futures_util::StreamExt; -use crate::auth::{JwtAuth, QueryUser, InsertUser}; +use crate::{auth::{JwtAuth, QueryUser, InsertUser}, error_handler::ServiceError, db::{upload_file, get_file, delete_file}}; -#[get("users/favorites")] +#[get("/favorites")] async fn get_favorites(auth: JwtAuth) -> HttpResponse { match QueryUser::get_by_email(&auth.user.email) { Ok(user) => { @@ -12,7 +14,7 @@ async fn get_favorites(auth: JwtAuth) -> HttpResponse { } } -#[post("users/favorites/{icao}")] +#[post("/favorites/{icao}")] async fn add_favorite(icao: web::Path, auth: JwtAuth) -> HttpResponse { match QueryUser::get_by_email(&auth.user.email) { Ok(user) => { @@ -33,7 +35,7 @@ async fn add_favorite(icao: web::Path, auth: JwtAuth) -> HttpResponse { } } -#[delete("users/favorites/{icao}")] +#[delete("/favorites/{icao}")] async fn delete_favorite(icao: web::Path, auth: JwtAuth) -> HttpResponse { let icao: String = icao.into_inner(); match QueryUser::get_by_email(&auth.user.email) { @@ -55,8 +57,95 @@ async fn delete_favorite(icao: web::Path, auth: JwtAuth) -> HttpResponse } } +#[post("/picture")] +async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse { + while let Some(item) = payload.next().await { + let mut bytes = web::BytesMut::new(); + let mut field = match item { + Ok(field) => field, + Err(err) => return ResponseError::error_response(&err) + }; + let content_type = field.content_disposition(); + let filename = match content_type.get_filename() { + Some(name) => { + match name.split(".").last() { + Some(ext) => { + match ext { + "apng" | "avif" | "gif" | "jpg" | "jpeg" | "jfif" | "pjpeg" | "pjp" | "png" | "svg" | "webp" => name, + _ => return ResponseError::error_response(&ServiceError::new(400, "File extension is not supported".to_string())) + } + }, + None => return ResponseError::error_response(&ServiceError::new(400, "Unknown file extension".to_string())) + } + }, + None => return ResponseError::error_response(&ServiceError::new(400, "File name is not provided".to_string())) + }; + let path = format!("users/{}/{}", auth.user.email, filename); + + while let Some(chunk) = field.next().await { + let data = match chunk { + Ok(data) => data, + Err(err) => return ResponseError::error_response(&err) + }; + bytes.extend_from_slice(&data); + } + match upload_file(&path, &bytes).await { + Ok(_) => { + match InsertUser::update_profile_picture(&auth.user.email, Some(&path)) { + Ok(_) => {}, + Err(err) => return ResponseError::error_response(&err) + }; + }, + Err(err) => return ResponseError::error_response(&err) + }; + } + HttpResponse::Ok().finish() +} + +#[get("/picture")] +async fn get_picture(auth: JwtAuth) -> HttpResponse { + let user = match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => user, + Err(err) => return ResponseError::error_response(&err) + }; + if let Some(path) = user.profile_picture { + match get_file(&path).await { + Ok(bytes) => HttpResponse::Ok().body(bytes), + Err(err) => ResponseError::error_response(&err) + } + } else { + HttpResponse::NotFound().finish() + } +} + +#[delete("/picture")] +async fn delete_picture(auth: JwtAuth) -> HttpResponse { + let user = match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => user, + Err(err) => return ResponseError::error_response(&err) + }; + if let Some(path) = user.profile_picture { + match delete_file(&path).await { + Ok(_) => { + match InsertUser::update_profile_picture(&auth.user.email, None) { + Ok(_) => HttpResponse::Ok().finish(), + Err(err) => ResponseError::error_response(&err) + } + }, + Err(err) => ResponseError::error_response(&err) + } + } else { + HttpResponse::NotFound().finish() + } +} + pub fn init_routes(config: &mut web::ServiceConfig) { - config.service(get_favorites); - config.service(add_favorite); - config.service(delete_favorite); + config.service(web::scope("users") + .service(get_favorites) + .service(add_favorite) + .service(delete_favorite) + .service(set_picture) + .service(get_picture) + .service(delete_picture) + ); } \ No newline at end of file diff --git a/ui/src/api/users.ts b/ui/src/api/users.ts index 399c061..b2dda5a 100644 --- a/ui/src/api/users.ts +++ b/ui/src/api/users.ts @@ -3,7 +3,7 @@ import { deleteRequest, getRequest, postRequest } from '.'; export async function getPicture(): Promise { const response = await getRequest('users/picture'); if (response?.status === 200) { - return response.blob(); + return await response.blob(); } else { return undefined; } diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx index 8d9f360..87471e7 100644 --- a/ui/src/app/profile/page.tsx +++ b/ui/src/app/profile/page.tsx @@ -4,9 +4,9 @@ import { getAirports } from "@/api/airport"; import { Airport } from "@/api/airport.types"; import { useEffect, useState } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; -import { Badge, Button, Card, Grid, Group, SimpleGrid, Text, Title } from "@mantine/core"; +import { Autocomplete, Badge, Box, Button, Card, Grid, Group, SimpleGrid, Text, Title } from "@mantine/core"; import classes from './profile.module.css'; -import { getFavorites, removeFavorite } from "@/api/users"; +import { addFavorite, getFavorites, removeFavorite } from "@/api/users"; import { getMetars } from "@/api/metar"; import { Metar } from "@/api/metar.types"; import { MdLocationSearching } from 'react-icons/md'; @@ -20,7 +20,7 @@ export default function Page() { return ( - + {user?.first_name} {user?.last_name} @@ -28,7 +28,7 @@ export default function Page() { - + @@ -40,6 +40,8 @@ export default function Page() { function TopSection() { const [airports, setAirports] = useState([]); const [metars, setMetars] = useState([]); + const [search, setSearch] = useState(''); + const [searchAirports, setSearchAirports] = useState([]); const router = useRouter(); const [_, setCoordinates] = useRecoilState(coordinatesState); @@ -96,6 +98,9 @@ function TopSection() { size="sm" radius="lg" style={{ marginTop: '10px' }} + onClick={() => { + router.push(`/airport/${airport.icao}`); + }} > View @@ -142,6 +147,28 @@ function TopSection() { Saved Airports
+ ({ value: a.icao, label: `${a.icao} - ${a.name}` }))} + limit={5} + style={{ paddingBottom: '10px' }} + onChange={async (value) => { + setSearch(value); + if (value) { + const a = await getAirports({ icaos: [value], name: value }); + setSearchAirports(a.data); + } + }} + onOptionSubmit={async (value) => { + if (!airports.find((a) => a.icao === value)) { + await addFavorite(value); + await updateFavorites(); + } + setSearch(''); + }} + /> {airports.map((airport) => AirportCard(airport))} diff --git a/ui/src/components/Header/HeaderModal.tsx b/ui/src/components/Header/HeaderModal.tsx index da13e19..74b1112 100644 --- a/ui/src/components/Header/HeaderModal.tsx +++ b/ui/src/components/Header/HeaderModal.tsx @@ -14,6 +14,7 @@ import { Text } from '@mantine/core'; import { useForm } from '@mantine/form'; +import Cookies from 'js-cookie'; interface HeaderModalProps { type?: string; @@ -72,9 +73,9 @@ export function HeaderModal({ type, toggle, login, register }: HeaderModalProps) const loginForm = useForm({ initialValues: { - email: '', + email: Cookies.get('email') || '', password: '', - remember: false + remember: Cookies.get('remember') === 'true' } }); @@ -173,6 +174,12 @@ export function HeaderModal({ type, toggle, login, register }: HeaderModalProps)
{ + Cookies.set('remember', 'true', { expires: 365 }); + if (values.remember) { + Cookies.set('email', values.email, { expires: 365 }); + } else { + Cookies.remove('email'); + } const success = await login(values); if (success) { onClose(); @@ -188,7 +195,7 @@ export function HeaderModal({ type, toggle, login, register }: HeaderModalProps) {...loginForm.getInputProps('password')} /> - + toggle('reset')}> Forgot password? diff --git a/ui/src/components/Header/header.css b/ui/src/components/Header/header.css index e517c45..4b2addf 100644 --- a/ui/src/components/Header/header.css +++ b/ui/src/components/Header/header.css @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; height: 46px; - background-color: #0057a3; + background-color: #242d3e; color: white; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index b1a2e9c..1b0f907 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -12,6 +12,8 @@ import { HeaderModal } from './HeaderModal'; import { coordinatesState } from '@/state/map'; import { User } from '@/api/auth.types'; import { usePathname, useRouter } from 'next/navigation'; +import { FaMoon } from "react-icons/fa6"; +import { FaSun } from "react-icons/fa6"; interface HeaderProps { user: User | undefined; @@ -106,6 +108,10 @@ function UserSection({ user, profilePicture, setProfilePicture, logout, toggle } return (
<> + {/* */} + {/* */} + {/* */} + {/* */} {user ? ( @@ -136,7 +142,7 @@ function UserSection({ user, profilePicture, setProfilePicture, logout, toggle } }); } }} - accept='image/png,image/jpeg,image/jpg' + accept='image/png,image/jpeg,image/svg+xml,image/webp,image/gif,image/apng,image/avif' multiple={false} > {(props) => ( diff --git a/ui/src/components/Loader.tsx b/ui/src/components/Loader.tsx index 6afbb92..3e0647c 100644 --- a/ui/src/components/Loader.tsx +++ b/ui/src/components/Loader.tsx @@ -1,36 +1,33 @@ 'use client'; -import { useEffect, useState } from "react"; -import Header from "./Header"; -import { useRecoilState } from "recoil"; -import { refreshIdState, userState } from "@/state/auth"; -import { login, logout, refresh, refreshLoggedIn, register } from "@/api/auth"; -import { getFavorites, getPicture } from "@/api/users"; -import Cookies from "js-cookie"; -import { favoritesState, profilePictureState } from "@/state/user"; -import { notifications } from "@mantine/notifications"; -import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useState } from 'react'; +import Header from './Header'; +import { useRecoilState } from 'recoil'; +import { refreshIdState, userState } from '@/state/auth'; +import { login, logout, refresh, refreshLoggedIn, register } from '@/api/auth'; +import { getFavorites, getPicture } from '@/api/users'; +import Cookies from 'js-cookie'; +import { favoritesState } from '@/state/user'; +import { notifications } from '@mantine/notifications'; +import { usePathname, useRouter } from 'next/navigation'; export default function Loader({ children }: { children: any }) { const [loading, setLoading] = useState(true); const [user, setUser] = useRecoilState(userState); const [refreshId, setRefreshId] = useRecoilState(refreshIdState); const [_, setFavorites] = useRecoilState(favoritesState); - const [profilePicture, setProfilePicture] = useRecoilState(profilePictureState); + const [profilePicture, setProfilePicture] = useState(undefined); const path = usePathname(); const router = useRouter(); useEffect(() => { - setLoading(true); - if (!user || !Cookies.get("logged_in")) { + if (!user || !Cookies.get('logged_in')) { refreshUser(); } - setLoading(false); - }, [user]); + }, []); useEffect(() => { const p = path.split('/'); - console.log(p[1], user); if (p.length > 1) { if (p[1] == 'admin' && user?.role != 'admin') { @@ -41,31 +38,36 @@ export default function Loader({ children }: { children: any }) { } }, [path]); - function refreshUser() { - refresh().then((response) => { - if (response) { - setRefreshId(refreshLoggedIn()); - setUser(response.user); - getFavorites().then((response) => { - if (response) { - setFavorites(response); - } - }); - if (response.user.profile_picture) { - getPicture().then((response) => { - if (response) { - setProfilePicture(response as File); - } - }); + async function refreshUser() { + setLoading(true); + const response = await refresh(); + if (response) { + setRefreshId(refreshLoggedIn()); + setUser(response.user); + const favoritesResponse = await getFavorites(); + if (favoritesResponse) { + setFavorites(favoritesResponse); + } + if (response.user.profile_picture) { + const pictureResponse = await getPicture(); + if (pictureResponse) { + setProfilePicture(pictureResponse as File); } } - }); + } + setLoading(false); } async function loginUser({ email, password }: { email: string, password: string}): Promise { const loginResponse = await login(email, password); if (loginResponse) { setUser(loginResponse.user); + if (loginResponse.user.profile_picture) { + const pictureResponse = await getPicture(); + if (pictureResponse) { + setProfilePicture(pictureResponse as File); + } + } setRefreshId(refreshLoggedIn()); notifications.show({ title: `Welcome back ${loginResponse.user.first_name}!`, @@ -89,7 +91,7 @@ export default function Loader({ children }: { children: any }) { async function logoutUser(): Promise { await logout(); - Cookies.remove("logged_in"); + Cookies.remove('logged_in'); setUser(undefined); setFavorites([]); setProfilePicture(undefined); @@ -117,6 +119,12 @@ export default function Loader({ children }: { children: any }) { const loginResponse = await login(email, password); if (loginResponse) { setUser(loginResponse.user); + if (loginResponse.user.profile_picture) { + const pictureResponse = await getPicture(); + if (pictureResponse) { + setProfilePicture(pictureResponse as File); + } + } setRefreshId(refreshLoggedIn()); notifications.update({ id, diff --git a/ui/src/state/user.ts b/ui/src/state/user.ts index 9e7bf43..6a16e5a 100644 --- a/ui/src/state/user.ts +++ b/ui/src/state/user.ts @@ -4,8 +4,3 @@ export const favoritesState = atom({ key: 'favoritesState', default: [] as string[] }); - -export const profilePictureState = atom({ - key: 'profilePictureState', - default: undefined as File | undefined -});