Working on email templating, updating with swagger
This commit is contained in:
14
.env
14
.env
@@ -2,11 +2,11 @@ RUST_LOG=warn,api=info
|
|||||||
|
|
||||||
NGINX_HOST=localhost
|
NGINX_HOST=localhost
|
||||||
NGINX_SSL_ENABLED=false
|
NGINX_SSL_ENABLED=false
|
||||||
NGINX_PROTOCOL=http
|
|
||||||
NGINX_HTTP_PORT=8080
|
NGINX_HTTP_PORT=8080
|
||||||
NGINX_HTTPS_PORT=8443
|
NGINX_HTTPS_PORT=8443
|
||||||
# Set to 'localhost' or 'host.docker.internal' or '172.17.0.1'
|
# Set to 'localhost' or 'host.docker.internal' or '172.17.0.1'
|
||||||
NGINX_INTERNAL_HOST=host.docker.internal
|
NGINX_INTERNAL_HOST=host.docker.internal
|
||||||
|
EXTERNAL_URL=http://localhost:8080
|
||||||
|
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
POSTGRES_USER=aviation
|
POSTGRES_USER=aviation
|
||||||
@@ -24,7 +24,7 @@ MINIO_BUCKET=aviation
|
|||||||
MINIO_PROTOCOL=http
|
MINIO_PROTOCOL=http
|
||||||
MINIO_PORT=9000
|
MINIO_PORT=9000
|
||||||
MINIO_INTERNAL_PORT=9001
|
MINIO_INTERNAL_PORT=9001
|
||||||
MINIO_BROWSER_REDIRECT_URL=${NGINX_PROTOCOL}://${NGINX_HOST}:${NGINX_HTTP_PORT}/minio/
|
MINIO_BROWSER_REDIRECT_URL=${EXTERNAL_URL}/minio/
|
||||||
|
|
||||||
UI_PORT=3000
|
UI_PORT=3000
|
||||||
API_PORT=5000
|
API_PORT=5000
|
||||||
@@ -35,11 +35,19 @@ SSL_CA_PATH=../ssl/${SSL_CA_NAME}.pem
|
|||||||
SSL_CERT_PATH=../ssl/localhost.crt
|
SSL_CERT_PATH=../ssl/localhost.crt
|
||||||
SSL_CERT_KEY_PATH=../ssl/localhost.key
|
SSL_CERT_KEY_PATH=../ssl/localhost.key
|
||||||
|
|
||||||
VITE_API_URL=${NGINX_PROTOCOL}://${NGINX_HOST}:${NGINX_HTTP_PORT}/api
|
SMTP_USERNAME=smtp-user
|
||||||
|
SMTP_PASSWORD=smtp-password
|
||||||
|
SMTP_FROM=noreply@example.com
|
||||||
|
SMTP_SERVER=smtp.example.com
|
||||||
|
|
||||||
|
VITE_API_URL=${EXTERNAL_URL}/api
|
||||||
VITE_DEFAULT_LIMIT=200
|
VITE_DEFAULT_LIMIT=200
|
||||||
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=${NGINX_HOST}
|
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=${NGINX_HOST}
|
||||||
|
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
API_CONTACT_NAME=changeme
|
||||||
|
API_CONTACT_EMAIL=contact@example.com
|
||||||
|
|
||||||
ADMIN_EMAIL=admin@example.com
|
ADMIN_EMAIL=admin@example.com
|
||||||
ADMIN_PASSWORD=changeme
|
ADMIN_PASSWORD=changeme
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<h1 align="center">Aviation Data</h1>
|
<h1 align="center">Aviation Data</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
[Swagger Docs](https://aviation.bensherriff.com/swagger/#/)
|
||||||
|
|
||||||
## Makefile
|
## Makefile
|
||||||
* `make` or `make help` to list all commands
|
* `make` or `make help` to list all commands
|
||||||
* `make docker-up` to start all containers
|
* `make docker-up` to start all containers
|
||||||
|
|||||||
445
api/Cargo.lock
generated
445
api/Cargo.lock
generated
@@ -254,6 +254,18 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy 0.8.24",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -362,6 +374,8 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"handlebars",
|
||||||
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
@@ -373,7 +387,20 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-actix-web",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -468,9 +495,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backon"
|
name = "backon"
|
||||||
version = "1.4.1"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "970d91570c01a8a5959b36ad7dd1c30642df24b6b3068710066f6809f7033bb7"
|
checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
]
|
]
|
||||||
@@ -609,6 +636,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chumsky"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"stacker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -817,6 +854,48 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.19"
|
version = "0.99.19"
|
||||||
@@ -904,6 +983,22 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email-encoding"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email_address"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@@ -1214,11 +1309,31 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "handlebars"
|
||||||
|
version = "6.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder",
|
||||||
|
"log",
|
||||||
|
"num-order",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -1279,6 +1394,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1637,6 +1763,7 @@ checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1715,6 +1842,34 @@ dependencies = [
|
|||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lettre"
|
||||||
|
version = "0.11.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"chumsky",
|
||||||
|
"email-encoding",
|
||||||
|
"email_address",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"hostname",
|
||||||
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.170"
|
version = "0.2.170"
|
||||||
@@ -1821,6 +1976,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minidom"
|
name = "minidom"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@@ -1868,6 +2033,15 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -1921,6 +2095,21 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-modular"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-order"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
|
||||||
|
dependencies = [
|
||||||
|
"num-modular",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -2067,6 +2256,51 @@ version = "2.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -2145,6 +2379,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psm"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -2164,6 +2407,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r2d2"
|
name = "r2d2"
|
||||||
version = "0.8.10"
|
version = "0.8.10"
|
||||||
@@ -2237,13 +2486,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "0.29.5"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f"
|
checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"backon",
|
"backon",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cfg-if",
|
||||||
"combine",
|
"combine",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2384,6 +2634,40 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60e425e204264b144d4c929d126d0de524b40a961686414bab5040f7465c71be"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2528,6 +2812,15 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.27"
|
version = "0.1.27"
|
||||||
@@ -2687,6 +2980,12 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -2946,6 +3245,19 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stacker"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"psm",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -3296,6 +3608,18 @@ 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 = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -3364,6 +3688,60 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa-gen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-actix-web"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
|
||||||
|
dependencies = [
|
||||||
|
"actix-service",
|
||||||
|
"actix-web",
|
||||||
|
"utoipa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-gen"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-swagger-ui"
|
||||||
|
version = "9.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"base64",
|
||||||
|
"mime_guess",
|
||||||
|
"regex",
|
||||||
|
"rust-embed",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"utoipa",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -3386,6 +3764,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3497,6 +3885,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@@ -3507,6 +3904,15 @@ dependencies = [
|
|||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -3518,9 +3924,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
@@ -3898,6 +4304,33 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
|
|||||||
@@ -23,10 +23,16 @@ tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }
|
|||||||
uuid = { version = "1.16.0", features = ["serde", "v4"] }
|
uuid = { version = "1.16.0", features = ["serde", "v4"] }
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
redis = { version = "0.29.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
redis = { version = "0.31.0", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
rust-s3 = "0.35.1"
|
rust-s3 = "0.35.1"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
rand_chacha = "0.9.0"
|
rand_chacha = "0.9.0"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
utoipa = { version = "5.3.1", features = ["chrono", "uuid", "actix_extras"] }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.1", features = ["actix-web"] }
|
||||||
|
utoipa-actix-web = "0.1.2"
|
||||||
|
webpki-roots = "1.0.0"
|
||||||
|
lettre = { version = "0.11.16", features = ["builder", "smtp-transport", "tokio1-native-tls"] }
|
||||||
|
handlebars = "6.3.2"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ CREATE INDEX ON airports (metar_observation_time);
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS runways (
|
CREATE TABLE IF NOT EXISTS runways (
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
icao TEXT NOT NULL,
|
icao TEXT NOT NULL REFERENCES airports(icao) ON DELETE CASCADE,
|
||||||
runway_id TEXT NOT NULL,
|
runway_id TEXT NOT NULL,
|
||||||
length_ft REAL NOT NULL,
|
length_ft REAL NOT NULL,
|
||||||
width_ft REAL NOT NULL,
|
width_ft REAL NOT NULL,
|
||||||
@@ -42,7 +42,7 @@ CREATE INDEX ON runways (runway_id);
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS communications (
|
CREATE TABLE IF NOT EXISTS communications (
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
icao TEXT NOT NULL,
|
icao TEXT NOT NULL REFERENCES airports(icao) ON DELETE CASCADE,
|
||||||
frequency_id TEXT NOT NULL,
|
frequency_id TEXT NOT NULL,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
frequencies_mhz REAL[] NOT NULL,
|
frequencies_mhz REAL[] NOT NULL,
|
||||||
@@ -64,7 +64,7 @@ CREATE TABLE IF NOT EXISTS metars (
|
|||||||
CREATE INDEX ON metars (observation_time DESC);
|
CREATE INDEX ON metars (observation_time DESC);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id UUID NOT NULL,
|
id UUID UNIQUE NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
email_verified BOOLEAN NOT NULL DEFAULT false,
|
email_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
indent_style = "Block"
|
indent_style = "Block"
|
||||||
reorder_imports = false
|
reorder_imports = true
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
|
use super::{SESSION_COOKIE_NAME, Session};
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use crate::{error::Error, users::User};
|
use crate::{error::Error, users::User};
|
||||||
use super::{Session, SESSION_COOKIE_NAME};
|
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Auth {
|
pub struct Auth {
|
||||||
@@ -34,13 +34,13 @@ impl FromRequest for Auth {
|
|||||||
return Err(Error::new(401, "API Key does not exist".to_string()).into());
|
return Err(Error::new(401, "API Key does not exist".to_string()).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match User::select(&api_key.id).await {
|
match User::select(&api_key.user_id).await {
|
||||||
Some(user) => Ok(Auth {
|
Some(user) => Ok(Auth {
|
||||||
session_id: None,
|
session_id: None,
|
||||||
api_key: Some(key_id),
|
api_key: Some(key_id),
|
||||||
user,
|
user,
|
||||||
}),
|
}),
|
||||||
None => Err(Error::new(404, format!("User {} not found", api_key.id)).into()),
|
None => Err(Error::new(404, format!("User {} not found", api_key.user_id)).into()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Box::pin(fut);
|
return Box::pin(fut);
|
||||||
@@ -79,13 +79,13 @@ impl FromRequest for Auth {
|
|||||||
// Verify the session
|
// Verify the session
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
match Session::verify(&session_id, &ip_address).await {
|
match Session::verify(&session_id, &ip_address).await {
|
||||||
Ok(session) => match User::select(&session.id).await {
|
Ok(session) => match User::select(&session.user_id).await {
|
||||||
Some(user) => Ok(Auth {
|
Some(user) => Ok(Auth {
|
||||||
session_id: Some(session_id),
|
session_id: Some(session_id),
|
||||||
api_key: None,
|
api_key: None,
|
||||||
user,
|
user,
|
||||||
}),
|
}),
|
||||||
None => Err(Error::new(404, format!("User {} not found", session.id)).into()),
|
None => Err(Error::new(404, format!("User {} not found", session.user_id)).into()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, SaltString},
|
|
||||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||||
|
password_hash::{SaltString, rand_core::OsRng},
|
||||||
};
|
};
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@@ -11,10 +11,10 @@ mod routes;
|
|||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
pub use session::*;
|
|
||||||
pub use routes::init_routes;
|
pub use routes::init_routes;
|
||||||
|
pub use session::*;
|
||||||
|
|
||||||
use crate::error::{Error, ApiResult};
|
use crate::error::{ApiResult, Error};
|
||||||
|
|
||||||
pub fn csprng(take: usize) -> String {
|
pub fn csprng(take: usize) -> String {
|
||||||
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
use actix_web::{post, web, HttpResponse, ResponseError, HttpRequest, put, get};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
account::{verify_hash, Session, SESSION_COOKIE_NAME},
|
account::{SESSION_COOKIE_NAME, Session, verify_hash},
|
||||||
error::Error,
|
error::Error,
|
||||||
|
smtp,
|
||||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||||
};
|
};
|
||||||
|
use actix_web::{HttpRequest, HttpResponse, ResponseError, get, post, put, web};
|
||||||
|
use utoipa_actix_web::scope;
|
||||||
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
use crate::account::Auth;
|
use crate::account::{Auth, csprng};
|
||||||
use crate::users::UpdateUser;
|
use crate::users::UpdateUser;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
request_body(
|
||||||
|
content = RegisterRequest, content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = UserResponse),
|
||||||
|
(status = 409, description = ""),
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||||
let register_user = user.into_inner();
|
let register_user = user.into_inner();
|
||||||
@@ -38,13 +51,22 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
|||||||
);
|
);
|
||||||
HttpResponse::Conflict().finish()
|
HttpResponse::Conflict().finish()
|
||||||
} else {
|
} else {
|
||||||
log::error!("attemptFailed to register user [Email: {}]: {}", email, err);
|
log::error!("Failed to register user [Email: {}]: {}", email, err);
|
||||||
ResponseError::error_response(&err)
|
ResponseError::error_response(&err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
request_body(
|
||||||
|
content = LoginRequest, content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = UserResponse),
|
||||||
|
),
|
||||||
|
)]
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
||||||
let email = &request.email;
|
let email = &request.email;
|
||||||
@@ -93,6 +115,17 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = ""),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
(status = 500, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[post("/logout")]
|
#[post("/logout")]
|
||||||
async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||||
let email = auth.user.email;
|
let email = auth.user.email;
|
||||||
@@ -132,6 +165,16 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = UserResponse),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[get("/profile")]
|
#[get("/profile")]
|
||||||
async fn get_profile(req: HttpRequest) -> HttpResponse {
|
async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||||
@@ -154,7 +197,7 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
|||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let id = &session.id;
|
let id = &session.user_id;
|
||||||
let query_user = match User::select(&id).await {
|
let query_user = match User::select(&id).await {
|
||||||
Some(query_user) => query_user,
|
Some(query_user) => query_user,
|
||||||
None => {
|
None => {
|
||||||
@@ -186,6 +229,16 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = ""),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[get("/session")]
|
#[get("/session")]
|
||||||
async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||||
@@ -208,7 +261,7 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let id = &session.id;
|
let id = &session.user_id;
|
||||||
let session_cookie = session.cookie();
|
let session_cookie = session.cookie();
|
||||||
let session_exp_cookie = session.expiration_cookie();
|
let session_exp_cookie = session.expiration_cookie();
|
||||||
|
|
||||||
@@ -229,6 +282,19 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Account",
|
||||||
|
request_body(
|
||||||
|
content = String, content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = UserResponse),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[put("/password")]
|
#[put("/password")]
|
||||||
async fn change_password(
|
async fn change_password(
|
||||||
password: web::Json<String>,
|
password: web::Json<String>,
|
||||||
@@ -269,25 +335,52 @@ async fn change_password(
|
|||||||
ip_address,
|
ip_address,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
ResponseError::error_response(&Error::new(500, err.to_string()))
|
ResponseError::error_response(&err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/password-reset")]
|
#[utoipa::path(
|
||||||
async fn password_reset(req: HttpRequest, _auth: Auth) -> HttpResponse {
|
tag = "Account",
|
||||||
let _ip_address = req.peer_addr().unwrap().ip().to_string();
|
responses(
|
||||||
|
(status = 200, description = ""),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[post("/password/reset")]
|
||||||
|
async fn reset_password(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||||
|
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||||
|
let id = auth.user.id;
|
||||||
|
let email = auth.user.email;
|
||||||
|
let token = csprng(128);
|
||||||
|
|
||||||
|
match smtp::send_password_reset(&email, &token) {
|
||||||
|
Ok(_) => HttpResponse::Ok().finish(),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"Invalid password reset attempt [ID: {}] [IP Address: {}]: {}",
|
||||||
|
&id,
|
||||||
|
ip_address,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
ResponseError::error_response(&err)
|
||||||
|
}
|
||||||
|
};
|
||||||
HttpResponse::Ok().finish()
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut ServiceConfig) {
|
||||||
config.service(
|
config.service(
|
||||||
web::scope("account")
|
scope::scope("/account")
|
||||||
.service(register)
|
.service(register)
|
||||||
.service(login)
|
.service(login)
|
||||||
.service(logout)
|
.service(logout)
|
||||||
.service(change_password)
|
|
||||||
.service(get_profile)
|
.service(get_profile)
|
||||||
.service(session_refresh),
|
.service(session_refresh)
|
||||||
|
.service(change_password)
|
||||||
|
.service(reset_password),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use actix_web::cookie::{time::Duration, Cookie};
|
use super::{csprng, hash, verify_hash};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use redis::{AsyncCommands, RedisResult};
|
|
||||||
use tokio::task;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::redis_async_connection,
|
db::redis_async_connection,
|
||||||
error::{Error, ApiResult},
|
error::{ApiResult, Error},
|
||||||
};
|
};
|
||||||
use super::{csprng, hash, verify_hash};
|
use actix_web::cookie::{Cookie, time::Duration};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use redis::{AsyncCommands, RedisResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::task;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||||
@@ -17,22 +17,22 @@ pub const SESSION_EXPIRATION_COOKIE_NAME: &str = "session_expiration";
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub ip_address: String,
|
pub ip_address: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub expires_at: Option<DateTime<Utc>>,
|
pub expires_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn default(id: &Uuid, ip_address: &str) -> Self {
|
pub fn default(user_id: &Uuid, ip_address: &str) -> Self {
|
||||||
Self::new(64, id, ip_address, Some(DEFAULT_SESSION_TTL))
|
Self::new(64, user_id, ip_address, Some(DEFAULT_SESSION_TTL))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(take: usize, id: &Uuid, ip_address: &str, ttl: Option<i64>) -> Self {
|
pub fn new(take: usize, user_id: &Uuid, ip_address: &str, ttl: Option<i64>) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
session_id: csprng(take),
|
session_id: csprng(take),
|
||||||
id: id.clone(),
|
user_id: user_id.clone(),
|
||||||
ip_address: hash(&ip_address).unwrap(),
|
ip_address: hash(&ip_address).unwrap(),
|
||||||
expires_at: match ttl {
|
expires_at: match ttl {
|
||||||
Some(ttl) => Some(now + chrono::Duration::seconds(ttl)),
|
Some(ttl) => Some(now + chrono::Duration::seconds(ttl)),
|
||||||
@@ -79,7 +79,7 @@ impl Session {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
session = Session::default(&session.id, ip_address);
|
session = Session::default(&session.user_id, ip_address);
|
||||||
session.store().await?;
|
session.store().await?;
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
@@ -120,8 +120,8 @@ impl Session {
|
|||||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||||
if environment == "development" || environment == "dev" {
|
if environment == "development" || environment == "dev" {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"Session cookie [ID: {}]: {}",
|
"Session cookie [User ID: {}]: {}",
|
||||||
self.id,
|
self.user_id,
|
||||||
self.session_id
|
self.session_id
|
||||||
);
|
);
|
||||||
cookie.set_secure(false);
|
cookie.set_secure(false);
|
||||||
@@ -148,8 +148,8 @@ impl Session {
|
|||||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||||
if environment == "development" || environment == "dev" {
|
if environment == "development" || environment == "dev" {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"Session expiration cookie [ID: {}]: {}",
|
"Session expiration cookie [User ID: {}]: {}",
|
||||||
self.id,
|
self.user_id,
|
||||||
self.session_id
|
self.session_id
|
||||||
);
|
);
|
||||||
cookie.set_secure(false);
|
cookie.set_secure(false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mod model;
|
pub mod model;
|
||||||
mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
pub use routes::init_routes;
|
pub use routes::init_routes;
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
use std::collections::HashMap;
|
use crate::airports::{
|
||||||
use std::str::FromStr;
|
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
|
||||||
|
UpdateRunway,
|
||||||
|
};
|
||||||
|
use crate::db;
|
||||||
|
use crate::error::{ApiResult, Error};
|
||||||
|
use crate::metars::Metar;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures_util::try_join;
|
use futures_util::try_join;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
use sqlx::{Postgres, QueryBuilder};
|
||||||
use crate::airports::{
|
use std::collections::HashMap;
|
||||||
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication, UpdateRunway,
|
use std::str::FromStr;
|
||||||
};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
use crate::db;
|
|
||||||
use crate::error::{ApiResult, Error};
|
|
||||||
use crate::metars::Metar;
|
|
||||||
|
|
||||||
const TABLE_NAME: &str = "airports";
|
const TABLE_NAME: &str = "airports";
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Airport {
|
pub struct Airport {
|
||||||
pub icao: String,
|
pub icao: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -40,7 +42,8 @@ pub struct Airport {
|
|||||||
pub latest_metar: Option<Metar>,
|
pub latest_metar: Option<Metar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
pub struct AirportQuery {
|
pub struct AirportQuery {
|
||||||
pub page: Option<u32>,
|
pub page: Option<u32>,
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
@@ -75,7 +78,7 @@ impl Default for AirportQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct Bounds {
|
pub struct Bounds {
|
||||||
pub north_east_lat: f32,
|
pub north_east_lat: f32,
|
||||||
pub north_east_lon: f32,
|
pub north_east_lon: f32,
|
||||||
@@ -125,7 +128,7 @@ struct AirportRow {
|
|||||||
pub metar_observation_time: Option<DateTime<Utc>>,
|
pub metar_observation_time: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct UpdateAirport {
|
pub struct UpdateAirport {
|
||||||
pub icao: Option<String>,
|
pub icao: Option<String>,
|
||||||
pub iata: Option<String>,
|
pub iata: Option<String>,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use serde::{Deserialize, Serialize};
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub enum AirportCategory {
|
pub enum AirportCategory {
|
||||||
#[serde(rename = "small_airport")]
|
#[serde(rename = "small_airport")]
|
||||||
Small,
|
Small,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::error::ApiResult;
|
use crate::error::ApiResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{Postgres, QueryBuilder};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
const TABLE_NAME: &str = "communications";
|
const TABLE_NAME: &str = "communications";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Communication {
|
pub struct Communication {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -27,7 +28,7 @@ pub struct CommunicationRow {
|
|||||||
pub phone: Option<String>,
|
pub phone: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct UpdateCommunication {
|
pub struct UpdateCommunication {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub icao: Option<String>,
|
pub icao: Option<String>,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::error::ApiResult;
|
use crate::error::ApiResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{Postgres, QueryBuilder};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
const TABLE_NAME: &str = "runways";
|
const TABLE_NAME: &str = "runways";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Runway {
|
pub struct Runway {
|
||||||
#[serde(rename = "id")]
|
#[serde(rename = "id")]
|
||||||
pub runway_id: String,
|
pub runway_id: String,
|
||||||
@@ -26,7 +27,7 @@ pub struct RunwayRow {
|
|||||||
pub surface: String,
|
pub surface: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct UpdateRunway {
|
pub struct UpdateRunway {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub icao: Option<String>,
|
pub icao: Option<String>,
|
||||||
|
|||||||
@@ -1,16 +1,39 @@
|
|||||||
use futures_util::stream::StreamExt as _;
|
use futures_util::stream::StreamExt as _;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
airports::Airport,
|
|
||||||
db::Paged,
|
|
||||||
account::{Auth, verify_role},
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
use actix_multipart::Multipart;
|
|
||||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
|
||||||
use crate::airports::{AirportQuery, UpdateAirport};
|
use crate::airports::{AirportQuery, UpdateAirport};
|
||||||
use crate::users::ADMIN_ROLE;
|
use crate::users::ADMIN_ROLE;
|
||||||
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
account::{Auth, verify_role},
|
||||||
|
airports::Airport,
|
||||||
|
db::Paged,
|
||||||
|
};
|
||||||
|
use actix_multipart::Multipart;
|
||||||
|
use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use utoipa_actix_web::scope;
|
||||||
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
|
#[derive(ToSchema)]
|
||||||
|
#[allow(unused)]
|
||||||
|
struct UploadedFile {
|
||||||
|
#[schema(value_type = String, format = Binary)]
|
||||||
|
file: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
request_body(
|
||||||
|
content = UploadedFile, content_type = "multipart/form-data"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful import"),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[post("/import")]
|
#[post("/import")]
|
||||||
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||||
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
|
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
|
||||||
@@ -53,6 +76,15 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
|||||||
HttpResponse::Ok().finish()
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
params(
|
||||||
|
AirportQuery
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = [Airport]),
|
||||||
|
),
|
||||||
|
)]
|
||||||
#[get("")]
|
#[get("")]
|
||||||
async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||||
@@ -87,6 +119,13 @@ async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpRespon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = Airport),
|
||||||
|
(status = 404, description = ""),
|
||||||
|
),
|
||||||
|
)]
|
||||||
#[get("/{icao}")]
|
#[get("/{icao}")]
|
||||||
async fn get_airport(
|
async fn get_airport(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
@@ -108,6 +147,17 @@ async fn get_airport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = Airport),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
(status = 409, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[post("")]
|
#[post("")]
|
||||||
async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||||
@@ -123,6 +173,16 @@ async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = Airport),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[put("/{icao}")]
|
#[put("/{icao}")]
|
||||||
async fn update_airport(
|
async fn update_airport(
|
||||||
icao: web::Path<String>,
|
icao: web::Path<String>,
|
||||||
@@ -142,6 +202,16 @@ async fn update_airport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
responses(
|
||||||
|
(status = 201, description = ""),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[delete("")]
|
#[delete("")]
|
||||||
async fn delete_airports(auth: Auth) -> HttpResponse {
|
async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||||
@@ -157,6 +227,16 @@ async fn delete_airports(auth: Auth) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Airports",
|
||||||
|
responses(
|
||||||
|
(status = 201, description = ""),
|
||||||
|
(status = 401, description = ""),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_auth" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[delete("/{icao}")]
|
#[delete("/{icao}")]
|
||||||
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||||
@@ -172,9 +252,9 @@ async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut ServiceConfig) {
|
||||||
config.service(
|
config.service(
|
||||||
web::scope("airports")
|
scope::scope("/airports")
|
||||||
.service(import_airports)
|
.service(import_airports)
|
||||||
.service(get_airports)
|
.service(get_airports)
|
||||||
.service(get_airport)
|
.service(get_airport)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::error::ApiResult;
|
use crate::error::ApiResult;
|
||||||
use redis::{Client as RedisClient, aio::MultiplexedConnection as RedisConnection, RedisResult};
|
use redis::{Client as RedisClient, RedisResult, aio::MultiplexedConnection as RedisConnection};
|
||||||
use s3::{Bucket, Region, creds::Credentials, BucketConfiguration, request::ResponseData};
|
use s3::{Bucket, BucketConfiguration, Region, creds::Credentials, request::ResponseData};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use sqlx::{Pool, Postgres};
|
|
||||||
use sqlx::postgres::PgPoolOptions;
|
|
||||||
|
|
||||||
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
|
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
|
||||||
static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
||||||
@@ -169,9 +169,3 @@ pub struct Paged<T> {
|
|||||||
pub limit: u32,
|
pub limit: u32,
|
||||||
pub total: i64,
|
pub total: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct Coordinate {
|
|
||||||
pub lon: f64,
|
|
||||||
pub lat: f64,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -204,3 +204,27 @@ impl From<sqlx::migrate::MigrateError> for Error {
|
|||||||
Error::new(500, error.to_string())
|
Error::new(500, error.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<lettre::address::AddressError> for Error {
|
||||||
|
fn from(error: lettre::address::AddressError) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lettre::error::Error> for Error {
|
||||||
|
fn from(error: lettre::error::Error) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lettre::transport::smtp::Error> for Error {
|
||||||
|
fn from(error: lettre::transport::smtp::Error) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Error {
|
||||||
|
fn from(error: String) -> Self {
|
||||||
|
Self::new(500, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
use std::env;
|
use crate::account::hash;
|
||||||
use std::time::Duration;
|
use crate::users::{ADMIN_ROLE, User};
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||||
use dotenv::from_filename;
|
use dotenv::from_filename;
|
||||||
use reqwest::Certificate;
|
use reqwest::Certificate;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
|
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||||
|
use utoipa::openapi::{Contact, SecurityRequirement};
|
||||||
|
use utoipa_actix_web::{AppExt, scope};
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::account::hash;
|
|
||||||
use crate::users::{User, ADMIN_ROLE};
|
|
||||||
|
|
||||||
mod account;
|
mod account;
|
||||||
mod airports;
|
mod airports;
|
||||||
@@ -14,6 +18,7 @@ mod db;
|
|||||||
mod error;
|
mod error;
|
||||||
mod metars;
|
mod metars;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
|
mod smtp;
|
||||||
mod system;
|
mod system;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
@@ -91,18 +96,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.allow_any_header()
|
.allow_any_header()
|
||||||
.supports_credentials()
|
.supports_credentials()
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
App::new()
|
let (app, mut api) = App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.app_data(web::Data::new(state.clone()))
|
.app_data(web::Data::new(state.clone()))
|
||||||
|
.into_utoipa_app()
|
||||||
.service(
|
.service(
|
||||||
web::scope("api")
|
scope::scope("/api")
|
||||||
.configure(airports::init_routes)
|
.configure(airports::init_routes)
|
||||||
.configure(metars::init_routes)
|
.configure(metars::init_routes)
|
||||||
.configure(account::init_routes)
|
.configure(account::init_routes)
|
||||||
.configure(users::init_routes)
|
.configure(users::init_routes)
|
||||||
.configure(system::init_routes),
|
.configure(system::init_routes),
|
||||||
)
|
)
|
||||||
|
.split_for_parts();
|
||||||
|
|
||||||
|
let contact_name = env::var("API_CONTACT_NAME").unwrap();
|
||||||
|
let contact_url = env::var("EXTERNAL_URL").unwrap();
|
||||||
|
let contact_email = env::var("API_CONTACT_EMAIL").unwrap();
|
||||||
|
let version = env::var("CARGO_PKG_VERSION").unwrap();
|
||||||
|
|
||||||
|
api.info.title = "Aviation Data".to_string();
|
||||||
|
api.info.description = None;
|
||||||
|
api.info.terms_of_service = None;
|
||||||
|
api.info.contact = Some(
|
||||||
|
Contact::builder()
|
||||||
|
.name(Some(contact_name))
|
||||||
|
.url(Some(format!("{}/support", contact_url)))
|
||||||
|
.email(Some(contact_email))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
api.info.license = None;
|
||||||
|
api.info.version = version;
|
||||||
|
|
||||||
|
let session_scheme = SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new("session")));
|
||||||
|
let mut components = api.components.take().unwrap_or_default();
|
||||||
|
components
|
||||||
|
.security_schemes
|
||||||
|
.insert("session_auth".to_string(), session_scheme);
|
||||||
|
api.components = Some(components);
|
||||||
|
// api.security = Some(vec![SecurityRequirement::new("session_auth", [""])]);
|
||||||
|
api.security = Some(vec![SecurityRequirement::default()]);
|
||||||
|
|
||||||
|
app.service(SwaggerUi::new("/swagger/{_:.*}").url("/api-docs/openapi.json", api))
|
||||||
})
|
})
|
||||||
.bind(format!("{}:{}", host, port))
|
.bind(format!("{}:{}", host, port))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use redis::{AsyncCommands, RedisResult};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::db::redis_async_connection;
|
use crate::db::redis_async_connection;
|
||||||
use crate::error::ApiResult;
|
use crate::error::ApiResult;
|
||||||
use crate::metars::Metar;
|
use crate::metars::Metar;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use redis::{AsyncCommands, RedisResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct MetarCheck {
|
pub struct MetarCheck {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ mod metar_check;
|
|||||||
mod model;
|
mod model;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use metar_check::*;
|
pub use metar_check::*;
|
||||||
|
pub use model::*;
|
||||||
pub use routes::init_routes;
|
pub use routes::init_routes;
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
|
use crate::airports::{Airport, UpdateAirport};
|
||||||
|
use crate::db::redis_async_connection;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::{error::ApiResult, db};
|
use crate::metars::MetarCheck;
|
||||||
|
use crate::{db, error::ApiResult};
|
||||||
use chrono::{DateTime, Datelike, NaiveDate, Utc};
|
use chrono::{DateTime, Datelike, NaiveDate, Utc};
|
||||||
|
use redis::{AsyncCommands, RedisResult};
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use redis::{AsyncCommands, RedisResult};
|
use utoipa::ToSchema;
|
||||||
use reqwest::Client;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::airports::{Airport, UpdateAirport};
|
|
||||||
use crate::db::redis_async_connection;
|
|
||||||
use crate::metars::MetarCheck;
|
|
||||||
|
|
||||||
const TABLE_NAME: &str = "metars";
|
const TABLE_NAME: &str = "metars";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Metar {
|
pub struct Metar {
|
||||||
pub icao: String,
|
pub icao: String,
|
||||||
pub raw_text: String,
|
pub raw_text: String,
|
||||||
@@ -60,7 +61,7 @@ pub struct Metar {
|
|||||||
pub density_altitude: Option<f64>,
|
pub density_altitude: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub enum ReportModifier {
|
pub enum ReportModifier {
|
||||||
#[serde(rename = "AUTO")]
|
#[serde(rename = "AUTO")]
|
||||||
Auto,
|
Auto,
|
||||||
@@ -88,7 +89,7 @@ impl Display for ReportModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct RunwayVisualRange {
|
pub struct RunwayVisualRange {
|
||||||
pub runway: String,
|
pub runway: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -110,7 +111,7 @@ impl Default for RunwayVisualRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub enum AutomatedStationType {
|
pub enum AutomatedStationType {
|
||||||
#[serde(rename = "AO1")]
|
#[serde(rename = "AO1")]
|
||||||
WithoutPrecipitationDiscriminator,
|
WithoutPrecipitationDiscriminator,
|
||||||
@@ -141,7 +142,7 @@ impl Display for AutomatedStationType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Remarks {
|
pub struct Remarks {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub peak_wind: Option<PeakWind>,
|
pub peak_wind: Option<PeakWind>,
|
||||||
@@ -165,7 +166,7 @@ pub struct Remarks {
|
|||||||
pub sky_condition_at_secondary_location_not_available: Option<String>,
|
pub sky_condition_at_secondary_location_not_available: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct PeakWind {
|
pub struct PeakWind {
|
||||||
pub degrees: i32,
|
pub degrees: i32,
|
||||||
pub speed: i32,
|
pub speed: i32,
|
||||||
@@ -190,7 +191,7 @@ impl Default for Remarks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SkyCondition {
|
pub struct SkyCondition {
|
||||||
pub sky_cover: String,
|
pub sky_cover: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -209,7 +210,7 @@ impl Default for SkyCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub enum FlightCategory {
|
pub enum FlightCategory {
|
||||||
VFR,
|
VFR,
|
||||||
MVFR,
|
MVFR,
|
||||||
@@ -1134,8 +1135,8 @@ impl Metar {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_time() {
|
fn test_parse_time() {
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
|
use crate::AppState;
|
||||||
use crate::metars::Metar;
|
use crate::metars::Metar;
|
||||||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::AppState;
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
struct FindAllParameters {
|
#[into_params(parameter_in = Query)]
|
||||||
|
struct MetarQuery {
|
||||||
icaos: Option<String>,
|
icaos: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("metars")]
|
#[utoipa::path(
|
||||||
|
tag = "METARs",
|
||||||
|
params(
|
||||||
|
MetarQuery,
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "", body = [Metar]),
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
#[get("/metars")]
|
||||||
async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let parameters = web::Query::<FindAllParameters>::from_query(req.query_string()).unwrap();
|
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
||||||
let icao_option = ¶meters.icaos;
|
let icao_option = ¶meters.icaos;
|
||||||
|
if let None = icao_option {
|
||||||
|
let empty_metars: Vec<Metar> = vec![];
|
||||||
|
return HttpResponse::Ok().json(empty_metars);
|
||||||
|
}
|
||||||
let icao_string = match icao_option {
|
let icao_string = match icao_option {
|
||||||
Some(i) => i,
|
Some(i) => i,
|
||||||
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
|
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
|
||||||
@@ -30,6 +46,6 @@ async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
|||||||
HttpResponse::Ok().json(metars)
|
HttpResponse::Ok().json(metars)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut ServiceConfig) {
|
||||||
config.service(find_all);
|
config.service(find_all);
|
||||||
}
|
}
|
||||||
|
|||||||
103
api/src/smtp/mod.rs
Normal file
103
api/src/smtp/mod.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use crate::error::ApiResult;
|
||||||
|
use chrono::{Datelike, Utc};
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use lettre::message::header::ContentType;
|
||||||
|
use lettre::message::{Mailbox, MultiPart, SinglePart};
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::{Address, Message, SmtpTransport, Transport};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static MAILER: OnceLock<SmtpTransport> = OnceLock::new();
|
||||||
|
static FROM_ADDRESS: OnceLock<Mailbox> = OnceLock::new();
|
||||||
|
static REGISTRY: OnceLock<Handlebars> = OnceLock::new();
|
||||||
|
|
||||||
|
fn mailer() -> &'static SmtpTransport {
|
||||||
|
MAILER.get_or_init(|| {
|
||||||
|
let server = env::var("SMTP_SERVER").expect("SMTP_SERVER missing");
|
||||||
|
let username = env::var("SMTP_USERNAME").expect("SMTP_USERNAME missing");
|
||||||
|
let password = env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD missing");
|
||||||
|
let creds = Credentials::new(username, password);
|
||||||
|
SmtpTransport::relay(&server)
|
||||||
|
.expect("invalid SMTP_SERVER")
|
||||||
|
.credentials(creds)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_address() -> &'static Mailbox {
|
||||||
|
FROM_ADDRESS.get_or_init(|| {
|
||||||
|
let raw = env::var("SMTP_FROM").expect("SMTP_FROM missing");
|
||||||
|
let addr = raw.parse().expect("SMTP_FROM invalid");
|
||||||
|
Mailbox::new(Some("Aviation Data".into()), addr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registry() -> &'static Handlebars<'static> {
|
||||||
|
REGISTRY.get_or_init(|| Handlebars::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PasswordResetCtx {
|
||||||
|
logo_url: String,
|
||||||
|
link: String,
|
||||||
|
domain: String,
|
||||||
|
year: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_password_reset(to: &str, token: &str) -> ApiResult<()> {
|
||||||
|
let base_url = env::var("EXTERNAL_URL")?.trim_end_matches('/').to_string();
|
||||||
|
let link = format!("{base_url}/profile/reset?token={token}");
|
||||||
|
let subject = "Reset your password";
|
||||||
|
|
||||||
|
let plain = format!(
|
||||||
|
"Hello,\n\n\
|
||||||
|
We received a password reset request. Click the link below:\n\n\
|
||||||
|
{link}\n\n\
|
||||||
|
This link expires in 24 hours. If you didn't request this, please ignore.\n\n\
|
||||||
|
Cheers,\n\
|
||||||
|
\tAviation Data",
|
||||||
|
link = link
|
||||||
|
);
|
||||||
|
|
||||||
|
let ctx = PasswordResetCtx {
|
||||||
|
logo_url: format!("{}/logo.svg", base_url),
|
||||||
|
link: link.clone(),
|
||||||
|
domain: base_url,
|
||||||
|
year: Utc::now().year(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let template_html = include_str!("../.././templates/password_reset.html");
|
||||||
|
let html = registry().render_template(template_html, &ctx).unwrap();
|
||||||
|
|
||||||
|
send_email(to, subject, plain, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiResult<()> {
|
||||||
|
let to_address = to.parse::<Address>()?;
|
||||||
|
let to_mailbox = Mailbox::new(None, to_address);
|
||||||
|
|
||||||
|
// Build the email
|
||||||
|
let email = Message::builder()
|
||||||
|
.from(from_address().clone())
|
||||||
|
.to(to_mailbox)
|
||||||
|
.subject(subject)
|
||||||
|
.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
|
.body(header),
|
||||||
|
)
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_HTML)
|
||||||
|
.body(html),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
mailer().send(&email)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,13 +1,22 @@
|
|||||||
use std::env;
|
use actix_web::{HttpResponse, get};
|
||||||
use actix_web::{get, web, HttpResponse};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use utoipa_actix_web::scope;
|
||||||
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SystemInfo {
|
pub struct SystemInfo {
|
||||||
version: String,
|
version: String,
|
||||||
healthy: bool,
|
healthy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "System",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful system info"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
#[get("/info")]
|
#[get("/info")]
|
||||||
async fn info() -> HttpResponse {
|
async fn info() -> HttpResponse {
|
||||||
let mut healthy = true;
|
let mut healthy = true;
|
||||||
@@ -24,6 +33,6 @@ async fn info() -> HttpResponse {
|
|||||||
HttpResponse::Ok().json(info)
|
HttpResponse::Ok().json(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut ServiceConfig) {
|
||||||
config.service(web::scope("/system").service(info));
|
config.service(scope::scope("/system").service(info));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
use crate::db;
|
||||||
|
use crate::{account::hash, error::ApiResult};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
use sqlx::{Postgres, QueryBuilder};
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::{account::hash, error::ApiResult};
|
|
||||||
use crate::db;
|
|
||||||
|
|
||||||
pub const ADMIN_ROLE: &str = "ADMIN";
|
pub const ADMIN_ROLE: &str = "ADMIN";
|
||||||
pub const USER_ROLE: &str = "USER";
|
pub const USER_ROLE: &str = "USER";
|
||||||
const TABLE_NAME: &str = "users";
|
const TABLE_NAME: &str = "users";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
@@ -35,13 +37,21 @@ impl RegisterRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
#[schema(
|
||||||
|
example = json!(
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "changeme"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
pub struct UserResponse {
|
pub struct UserResponse {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
@@ -65,7 +75,7 @@ impl From<User> for UserResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Deserialize, sqlx::FromRow, ToSchema)]
|
||||||
pub struct UpdateUser {
|
pub struct UpdateUser {
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub email_verified: Option<bool>,
|
pub email_verified: Option<bool>,
|
||||||
|
|||||||
@@ -152,7 +152,9 @@
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn init_routes(_config: &mut actix_web::web::ServiceConfig) {
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
|
pub fn init_routes(_config: &mut ServiceConfig) {
|
||||||
// config.service(
|
// config.service(
|
||||||
// web::scope("users")
|
// web::scope("users")
|
||||||
// .service(get_favorites)
|
// .service(get_favorites)
|
||||||
|
|||||||
53
api/templates/password_reset.html
Normal file
53
api/templates/password_reset.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||||
|
<title>Reset your password</title>
|
||||||
|
<style>
|
||||||
|
body { margin:0; padding:0; background:#f2f2f2; font-family:Helvetica,Arial,sans-serif; }
|
||||||
|
.wrapper { width:100%; table-layout:fixed; background:#f2f2f2; padding:40px 0; }
|
||||||
|
.main { background:#ffffff; width:600px; margin:0 auto; border-radius:6px; overflow:hidden; }
|
||||||
|
.header { background:#fff; text-align:center; padding:30px; }
|
||||||
|
.header img { width:60px; height:auto; display:block; margin:0 auto 10px; }
|
||||||
|
.header h1 { margin:0; font-size:24px; color:#333333; }
|
||||||
|
.header p { margin:5px 0 0; font-size:14px; color:#777777; }
|
||||||
|
.content { padding:30px; color:#333333; font-size:16px; line-height:1.5; }
|
||||||
|
.content h2 { margin-top:0; font-size:20px; }
|
||||||
|
.btn-wrap { text-align:center; margin:30px 0; }
|
||||||
|
.btn { background:#28a745; color:#ffffff !important; text-decoration:none; padding:12px 24px; border-radius:4px; display:inline-block; font-size:16px; }
|
||||||
|
.footer { text-align:center; padding:20px; font-size:12px; color:#999999; }
|
||||||
|
.footer a { color:#999999; text-decoration:none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- header -->
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{logo_url}}" alt="Aviation Data Logo" />
|
||||||
|
<h1>Aviation Data</h1>
|
||||||
|
<p>Your source for aviation data</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- body -->
|
||||||
|
<div class="content">
|
||||||
|
<h2>Reset Your Password</h2>
|
||||||
|
<p>We received a request to reset your password. Click the button below to choose a new one:</p>
|
||||||
|
<div class="btn-wrap">
|
||||||
|
<a href="{{link}}" class="btn">Reset my password</a>
|
||||||
|
</div>
|
||||||
|
<p>If you didn’t request this reset, you can safely ignore this email.</p>
|
||||||
|
<p>Cheers,<br/>The Aviation Data Team</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- footer -->
|
||||||
|
<div class="footer">
|
||||||
|
Serving the Enthusiast Community<br/>
|
||||||
|
<a href="{{domain}}">{{domain}}</a> | © {{year}} Aviation Data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Get Profile
|
name: Get Profile
|
||||||
type: http
|
type: http
|
||||||
seq: 6
|
seq: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Refresh Session
|
name: Refresh Session
|
||||||
type: http
|
type: http
|
||||||
seq: 5
|
seq: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
11
bruno/Users/Reset Password.bru
Normal file
11
bruno/Users/Reset Password.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Reset Password
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{API_URL}}/account/password/reset
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -12,6 +12,22 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /api-docs/openapi.json {
|
||||||
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${API_PORT}/api-docs/openapi.json;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /swagger/ {
|
||||||
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${API_PORT}/swagger/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /minio/ {
|
location /minio/ {
|
||||||
proxy_pass http://${NGINX_INTERNAL_HOST}:${MINIO_INTERNAL_PORT}/;
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${MINIO_INTERNAL_PORT}/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /api-docs/openapi.json {
|
||||||
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${API_PORT}/api-docs/openapi.json;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /swagger/ {
|
||||||
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${API_PORT}/swagger/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /minio/ {
|
location /minio/ {
|
||||||
proxy_pass http://${NGINX_INTERNAL_HOST}:${MINIO_INTERNAL_PORT}/;
|
proxy_pass http://${NGINX_INTERNAL_HOST}:${MINIO_INTERNAL_PORT}/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<!-- The config file only exists in production environments -->
|
||||||
<script src="./config.js"></script>
|
<script src="./config.js"></script>
|
||||||
<script type="module" src="./src/main.tsx"></script>
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function CommunicationTable({ communications }: { communications: Communi
|
|||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>ID</Table.Th>
|
<Table.Th>ID</Table.Th>
|
||||||
<Table.Th>Name</Table.Th>
|
<Table.Th>Name</Table.Th>
|
||||||
<Table.Th>Frequencies (MHz)</Table.Th>
|
<Table.Th>MHz</Table.Th>
|
||||||
<Table.Th>Phone</Table.Th>
|
<Table.Th>Phone</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|||||||
@@ -151,10 +151,10 @@ function AirportInfoRow({ style, children }: { style?: CSSProperties; children:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AirportInfo({ map, airport }: { map: LeafletMap, airport: Airport }) {
|
function AirportInfo({ map, airport }: { map: LeafletMap; airport: Airport }) {
|
||||||
function goToLocation(map: LeafletMap, latitude: number, longitude: number) {
|
function goToLocation(map: LeafletMap, latitude: number, longitude: number) {
|
||||||
if (!map) return
|
if (!map) return;
|
||||||
map.setView([latitude, longitude], map.getZoom())
|
map.setView([latitude, longitude], map.getZoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,9 +171,11 @@ function AirportInfo({ map, airport }: { map: LeafletMap, airport: Airport }) {
|
|||||||
</AirportInfoSlot>
|
</AirportInfoSlot>
|
||||||
<AirportInfoSlot title={'Elevation'} style={{ paddingLeft: '1rem' }} children={`${airport.elevation_ft} ft`} />
|
<AirportInfoSlot title={'Elevation'} style={{ paddingLeft: '1rem' }} children={`${airport.elevation_ft} ft`} />
|
||||||
<AirportInfoSlot style={{ marginLeft: 'auto', paddingLeft: '1rem', paddingTop: '0.5rem' }}>
|
<AirportInfoSlot style={{ marginLeft: 'auto', paddingLeft: '1rem', paddingTop: '0.5rem' }}>
|
||||||
<UnstyledButton onClick={() => {
|
<UnstyledButton
|
||||||
goToLocation(map, airport.latitude, airport.longitude)
|
onClick={() => {
|
||||||
}}>
|
goToLocation(map, airport.latitude, airport.longitude);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IconViewfinder />
|
<IconViewfinder />
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</AirportInfoSlot>
|
</AirportInfoSlot>
|
||||||
@@ -181,9 +183,7 @@ function AirportInfo({ map, airport }: { map: LeafletMap, airport: Airport }) {
|
|||||||
<Accordion chevronPosition={'right'} variant={'contained'}>
|
<Accordion chevronPosition={'right'} variant={'contained'}>
|
||||||
{airport.runways != null && airport.runways.length > 0 && (
|
{airport.runways != null && airport.runways.length > 0 && (
|
||||||
<Accordion.Item value={'runways'}>
|
<Accordion.Item value={'runways'}>
|
||||||
<Accordion.Control>
|
<Accordion.Control>Runways</Accordion.Control>
|
||||||
Runways
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<RunwayTable runways={airport.runways} />
|
<RunwayTable runways={airport.runways} />
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
@@ -191,9 +191,7 @@ function AirportInfo({ map, airport }: { map: LeafletMap, airport: Airport }) {
|
|||||||
)}
|
)}
|
||||||
{airport.communications != null && airport.communications.length > 0 && (
|
{airport.communications != null && airport.communications.length > 0 && (
|
||||||
<Accordion.Item value={'communication'}>
|
<Accordion.Item value={'communication'}>
|
||||||
<Accordion.Control>
|
<Accordion.Control>Communication</Accordion.Control>
|
||||||
Communication
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<CommunicationTable communications={airport.communications} />
|
<CommunicationTable communications={airport.communications} />
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function CustomControl({ position = 'bottomright', onClick, active = fals
|
|||||||
const ctrl = new L.Control({ position });
|
const ctrl = new L.Control({ position });
|
||||||
ctrl.onAdd = () => {
|
ctrl.onAdd = () => {
|
||||||
return L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
|
return L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
|
||||||
}
|
};
|
||||||
|
|
||||||
ctrl.addTo(map);
|
ctrl.addTo(map);
|
||||||
controlRef.current = ctrl;
|
controlRef.current = ctrl;
|
||||||
@@ -28,6 +28,7 @@ export function CustomControl({ position = 'bottomright', onClick, active = fals
|
|||||||
// @ts-expect-error ctrl is a L.Control
|
// @ts-expect-error ctrl is a L.Control
|
||||||
const container = (ctrl as unknown)._container as HTMLElement;
|
const container = (ctrl as unknown)._container as HTMLElement;
|
||||||
rootRef.current = createRoot(container);
|
rootRef.current = createRoot(container);
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (rootRef.current) {
|
if (rootRef.current) {
|
||||||
@@ -45,7 +46,7 @@ export function CustomControl({ position = 'bottomright', onClick, active = fals
|
|||||||
href={'#'}
|
href={'#'}
|
||||||
title={title}
|
title={title}
|
||||||
className={active ? 'active' : ''}
|
className={active ? 'active' : ''}
|
||||||
onClick={e => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick();
|
onClick();
|
||||||
@@ -59,7 +60,7 @@ export function CustomControl({ position = 'bottomright', onClick, active = fals
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}, [onClick, active, title, children]);
|
}, [onClick, active, title, children]);
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function GroupControl({ position = 'bottomright', buttons }: GroupControl
|
|||||||
// @ts-expect-error ctrl is a L.Control
|
// @ts-expect-error ctrl is a L.Control
|
||||||
const container = (ctrl as unknown)._container as HTMLElement;
|
const container = (ctrl as unknown)._container as HTMLElement;
|
||||||
rootRef.current = createRoot(container);
|
rootRef.current = createRoot(container);
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ctrl.remove();
|
ctrl.remove();
|
||||||
@@ -48,10 +49,10 @@ export function GroupControl({ position = 'bottomright', buttons }: GroupControl
|
|||||||
{buttons.map((b, i) => (
|
{buttons.map((b, i) => (
|
||||||
<a
|
<a
|
||||||
key={i}
|
key={i}
|
||||||
href="#"
|
href='#'
|
||||||
title={b.title}
|
title={b.title}
|
||||||
className={b.active ? 'active' : ''}
|
className={b.active ? 'active' : ''}
|
||||||
onClick={e => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
b.onClick();
|
b.onClick();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function LocateControl() {
|
|||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
alert('Geolocation is not supported by your browser');
|
console.warn('Geolocation is not supported by your browser');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
@@ -17,14 +17,13 @@ export function LocateControl() {
|
|||||||
map.setView([latitude, longitude], map.getZoom());
|
map.setView([latitude, longitude], map.getZoom());
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error(err);
|
console.warn('Unable to retrieve your location', err);
|
||||||
alert('Unable to retrieve your location');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomControl onClick={handleClick} title="Go to my location">
|
<CustomControl onClick={handleClick} title='Go to my location'>
|
||||||
<IconCurrentLocation />
|
<IconCurrentLocation />
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import { UserContext } from './UserContext.tsx';
|
|||||||
import { profile } from '@lib/account.ts';
|
import { profile } from '@lib/account.ts';
|
||||||
import { User } from '@lib/account.types.ts';
|
import { User } from '@lib/account.types.ts';
|
||||||
import { Center, Loader } from '@mantine/core';
|
import { Center, Loader } from '@mantine/core';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
const sessionExpirationName = 'session_expiration';
|
||||||
|
|
||||||
export function UserProvider({ children }: { children: ReactNode }) {
|
export function UserProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | undefined>(undefined);
|
const [user, setUser] = useState<User | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const sessionExpiration = Cookies.get(sessionExpirationName);
|
||||||
|
|
||||||
|
if (sessionExpiration != undefined) {
|
||||||
|
const date = new Date(parseInt(sessionExpiration) * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
if (date > now) {
|
||||||
profile().then((refreshUser) => {
|
profile().then((refreshUser) => {
|
||||||
if (refreshUser) {
|
if (refreshUser) {
|
||||||
setUser(refreshUser);
|
setUser(refreshUser);
|
||||||
@@ -17,6 +26,13 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Cookies.remove(sessionExpirationName);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user