Overhaul
This commit is contained in:
2
Makefile
2
Makefile
@@ -124,7 +124,7 @@ push: image=${registry}/aviation-${folder}:${version}
|
||||
push: ## Build and push a specific docker image from a folder
|
||||
docker buildx create \
|
||||
--use \
|
||||
--name aviation-builder \
|
||||
--name default-builder \
|
||||
--platform ${platform} || true; \
|
||||
docker buildx build \
|
||||
-f ${folder}/Dockerfile \
|
||||
|
||||
91
Taskfile.yml
Normal file
91
Taskfile.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
dotenv: ['.env.local', '.env']
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: docker-up
|
||||
silent: true
|
||||
|
||||
test:
|
||||
cmds:
|
||||
- task: docker-backend
|
||||
- task: dev-servers
|
||||
|
||||
dev-servers:
|
||||
deps:
|
||||
- task: run-api
|
||||
- task: run-ui
|
||||
|
||||
# API Commands
|
||||
build-api:
|
||||
dir: api
|
||||
cmds:
|
||||
- cargo build
|
||||
format-api:
|
||||
dir: api
|
||||
cmds:
|
||||
- cargo fmt
|
||||
run-api:
|
||||
dir: api
|
||||
cmds:
|
||||
- cargo run
|
||||
silent: true
|
||||
|
||||
# UI Commands
|
||||
build-ui:
|
||||
dir: ui
|
||||
cmds:
|
||||
- npm run build
|
||||
format-ui:
|
||||
dir: ui
|
||||
cmds:
|
||||
- npm run format
|
||||
clean-ui:
|
||||
dir: ui
|
||||
cmds:
|
||||
- rm -rf node_modules dist stats.html
|
||||
run-ui:
|
||||
dir: ui
|
||||
cmds:
|
||||
- npm run dev
|
||||
silent: true
|
||||
|
||||
# Docker Commands
|
||||
docker-backend:
|
||||
cmds:
|
||||
- docker compose --profile backend up -d
|
||||
docker-up:
|
||||
cmds:
|
||||
- docker compose --profile backend --profile api up -d
|
||||
docker-down:
|
||||
cmds:
|
||||
- docker compose --profile backend --profile api down
|
||||
docker-clean:
|
||||
cmds:
|
||||
- docker compose --profile backend --profile api down -v
|
||||
docker-refresh:
|
||||
cmds:
|
||||
- task: docker-clean
|
||||
- task: docker-up
|
||||
docker-build:
|
||||
cmds:
|
||||
- docker compose build
|
||||
|
||||
psql:
|
||||
cmds:
|
||||
- docker exec -it aviation-postgres psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -P pager=off
|
||||
|
||||
cert:
|
||||
cmds:
|
||||
- ./scripts/generate_ca_cert.sh
|
||||
- ./scripts/generate_server_cert.sh ${TLS_HOST} nginx
|
||||
- ./scripts/generate_server_cert.sh ${API_HOST} api
|
||||
silent: true
|
||||
cert-clean:
|
||||
cmds:
|
||||
- rm -rf ./data/certificates
|
||||
silent: true
|
||||
229
api/Cargo.lock
generated
229
api/Cargo.lock
generated
@@ -64,7 +64,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
@@ -158,7 +158,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"socket2",
|
||||
"socket2 0.5.9",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -220,7 +220,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
"socket2 0.5.9",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -295,12 +295,6 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -362,7 +356,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "api"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-multipart",
|
||||
@@ -378,7 +372,7 @@ dependencies = [
|
||||
"handlebars",
|
||||
"lettre",
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"redis",
|
||||
"regex",
|
||||
@@ -496,9 +490,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.5.0"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496"
|
||||
checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
]
|
||||
@@ -624,17 +618,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1092,9 +1085,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
@@ -1297,9 +1290,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbe789d04bf14543f03c4b60cd494148aa79438c8440ae7d81a7778147745c3"
|
||||
checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dashmap",
|
||||
@@ -1312,7 +1305,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"quanta",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"smallvec",
|
||||
"spinning_top",
|
||||
"web-time",
|
||||
@@ -1449,7 +1442,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1536,7 +1529,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.9",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -1615,18 +1608,23 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.9",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1783,12 +1781,33 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@@ -1862,9 +1881,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.16"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2"
|
||||
checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
@@ -1882,7 +1901,7 @@ dependencies = [
|
||||
"nom",
|
||||
"percent-encoding",
|
||||
"quoted_printable",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"url",
|
||||
@@ -1890,9 +1909,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -1912,9 +1931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
|
||||
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
@@ -1960,9 +1979,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-async"
|
||||
@@ -2494,9 +2513,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -2551,9 +2570,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.31.0"
|
||||
version = "0.32.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f"
|
||||
checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"backon",
|
||||
@@ -2571,7 +2590,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1_smol",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
@@ -2588,9 +2607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2623,15 +2642,14 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.15"
|
||||
version = "0.12.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.10",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
@@ -2640,29 +2658,26 @@ dependencies = [
|
||||
"hyper-rustls",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2822,15 +2837,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@@ -2964,9 +2970,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
version = "1.0.142"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3093,6 +3099,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -3123,9 +3139,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
|
||||
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -3136,9 +3152,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -3172,9 +3188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
|
||||
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3185,9 +3201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
|
||||
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -3204,16 +3220,15 @@ dependencies = [
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
|
||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
@@ -3255,9 +3270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
|
||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
@@ -3294,9 +3309,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
|
||||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -3538,20 +3553,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.47.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"slab",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3624,6 +3641,24 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -3762,9 +3797,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.3.1"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -3785,9 +3820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.3.1"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3816,12 +3851,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3969,9 +4006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -4025,7 +4062,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
@@ -4058,6 +4095,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
@@ -4075,7 +4118,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4084,7 +4127,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4093,7 +4136,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4443,9 +4486,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
|
||||
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2024"
|
||||
authors = ["Ben Sherriff <ben@bensherriff.com>"]
|
||||
repository = "https://gitea.bensherriff.com/bsherriff/aviation"
|
||||
@@ -9,32 +9,32 @@ readme = "../README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.10.2"
|
||||
actix-web = "4.11.0"
|
||||
actix-cors = "0.7.1"
|
||||
actix-multipart = "0.7.2"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
dotenv = "0.15.0"
|
||||
sqlx = { version = "0.8.5", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
|
||||
env_logger = "0.11.8"
|
||||
reqwest = "0.12.15"
|
||||
reqwest = "0.12.23"
|
||||
serde = {version = "1.0.219", features = ["derive"]}
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.45.0", features = ["macros", "rt", "time"] }
|
||||
uuid = { version = "1.16.0", features = ["serde", "v4"] }
|
||||
serde_json = "1.0.142"
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt", "time"] }
|
||||
uuid = { version = "1.18.0", features = ["serde", "v4"] }
|
||||
log = "0.4.27"
|
||||
argon2 = "0.5.3"
|
||||
redis = { version = "0.31.0", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
||||
redis = { version = "0.32.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
||||
regex = "1.11.1"
|
||||
futures-util = "0.3.31"
|
||||
rust-s3 = "0.35.1"
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
rand_chacha = "0.9.0"
|
||||
futures = "0.3.31"
|
||||
utoipa = { version = "5.3.1", features = ["chrono", "uuid", "actix_extras"] }
|
||||
utoipa = { version = "5.4.0", features = ["chrono", "uuid", "actix_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", 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"] }
|
||||
lettre = { version = "0.11.18", features = ["builder", "smtp-transport", "tokio1-native-tls"] }
|
||||
handlebars = "6.3.2"
|
||||
governor = "0.10.0"
|
||||
flate2 = "1.1.1"
|
||||
governor = "0.10.1"
|
||||
flate2 = "1.1.2"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use std::sync::Arc;
|
||||
use super::{SESSION_COOKIE_NAME, Session};
|
||||
use crate::account::user::User;
|
||||
use crate::error::Error;
|
||||
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http};
|
||||
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Auth {
|
||||
@@ -19,23 +20,31 @@ impl FromRequest for Auth {
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
// Check for API key
|
||||
let state = match req.app_data::<web::Data<AppState>>() {
|
||||
Some(state) => state,
|
||||
None => return Box::pin(
|
||||
async { Err(Error::new(500, "Internal server error".to_string()).into()) },
|
||||
)
|
||||
};
|
||||
|
||||
// Check for an API key
|
||||
match req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.map(|h| h.to_str().unwrap().split_at(7).1.to_string())
|
||||
{
|
||||
Some(key_id) => {
|
||||
let state = Arc::clone(&state);
|
||||
let fut = async move {
|
||||
// Check if the Session API key exists
|
||||
let api_key = match Session::get(&key_id).await {
|
||||
let api_key = match Session::get(&state, &key_id).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
log::error!("Invalid session auth attempt: {}", err);
|
||||
return Err(Error::new(401, "API Key does not exist".to_string()).into());
|
||||
}
|
||||
};
|
||||
match User::select(&api_key.username).await {
|
||||
match User::select(&state.pool, &api_key.username).await {
|
||||
Some(user) => Ok(Auth {
|
||||
session_id: None,
|
||||
api_key: Some(key_id),
|
||||
@@ -78,9 +87,10 @@ impl FromRequest for Auth {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
// Verify the session
|
||||
let state = Arc::clone(&state); // state: Arc<State>
|
||||
let fut = async move {
|
||||
match Session::verify(&session_id, &ip_address).await {
|
||||
Ok(session) => match User::select(&session.username).await {
|
||||
match Session::verify(&state, &session_id, &ip_address).await {
|
||||
Ok(session) => match User::select(&state.pool, &session.username).await {
|
||||
Some(user) => Ok(Auth {
|
||||
session_id: Some(session_id),
|
||||
api_key: None,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::account::{csprng, hash};
|
||||
use crate::db::redis_async_connection;
|
||||
use crate::error::{ApiResult, Error};
|
||||
use crate::smtp;
|
||||
use chrono::{Datelike, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::{env, fs};
|
||||
use redis::aio::ConnectionManager;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EmailToken {
|
||||
@@ -24,37 +24,25 @@ impl EmailToken {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store(&self, ttl_secs: i64) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
pub async fn store(&self, state: &AppState, ttl_secs: i64) -> ApiResult<()> {
|
||||
let key = self.token.clone();
|
||||
let value = serde_json::to_string(self)?;
|
||||
let now = Utc::now();
|
||||
let expires_at = now + chrono::Duration::seconds(ttl_secs);
|
||||
let ttl = expires_at.timestamp() - now.timestamp();
|
||||
let result: RedisResult<()> = conn.set_ex(key, &value, ttl as u64).await;
|
||||
state.set_ex(&key, &value, ttl as u64).await
|
||||
}
|
||||
|
||||
pub async fn get(state: &AppState, token: &str) -> ApiResult<Self> {
|
||||
let result: Option<String> = state.get(token).await?;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.into()),
|
||||
Some(value) => Ok(serde_json::from_str(&value)?),
|
||||
None => Err(Error::new(404, format!("Missing email token {}", token))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(token: &str) -> ApiResult<Self> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<Option<String>> = conn.get(token).await;
|
||||
match result {
|
||||
Ok(Some(value)) => Ok(serde_json::from_str(&value)?),
|
||||
Ok(None) => Err(Error::new(404, format!("Missing email token {}", token))),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(token: &str) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<()> = conn.del(token).await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
pub async fn delete(state: &AppState, token: &str) -> ApiResult<()> {
|
||||
state.del(token).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +101,10 @@ pub async fn send_password_reset_email(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_confirm_email(email: &str, ip_address: &str) -> ApiResult<()> {
|
||||
pub async fn send_confirm_email(state: &AppState, email: &str, ip_address: &str) -> ApiResult<()> {
|
||||
let token = csprng(128);
|
||||
let email_token = EmailToken::new(email.to_string(), token, &ip_address);
|
||||
email_token.store(86400).await?;
|
||||
email_token.store(state, 86400).await?;
|
||||
|
||||
let base_url = env::var("EXTERNAL_URL")?;
|
||||
let link = format!("{base_url}/profile/confirm?token={}", email_token.token);
|
||||
@@ -153,7 +141,7 @@ pub async fn send_confirm_email(email: &str, ip_address: &str) -> ApiResult<()>
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
let _ = EmailToken::delete(&email_token.token);
|
||||
let _ = EmailToken::delete(state, &email_token.token);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::account::email_token::{EmailToken, send_confirm_email, send_password_
|
||||
use crate::account::user::{LoginRequest, RegisterRequest, UpdateUser, User, UserResponse};
|
||||
use crate::account::user_favorites::UserFavorite;
|
||||
use crate::account::{Auth, csprng};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "account",
|
||||
@@ -24,7 +25,7 @@ use crate::account::{Auth, csprng};
|
||||
)
|
||||
)]
|
||||
#[post("/register")]
|
||||
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||
async fn register(state: web::Data<AppState>, user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let register_user = user.into_inner();
|
||||
let username = register_user.username.clone();
|
||||
let email = register_user.email.clone();
|
||||
@@ -34,7 +35,7 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
|
||||
match insert_user.insert().await {
|
||||
match insert_user.insert(&state.pool).await {
|
||||
Ok(user) => {
|
||||
let user_response: UserResponse = user.into();
|
||||
log::info!(
|
||||
@@ -46,8 +47,8 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
// Send confirmation email
|
||||
if let Some(email) = email {
|
||||
if !email.is_empty() {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = send_confirm_email(&email, &ip_address).await {
|
||||
tokio::task::spawn_local(async move {
|
||||
if let Err(err) = send_confirm_email(&state, &email, &ip_address).await {
|
||||
log::error!("Failed to send confirmation email: {}", err);
|
||||
};
|
||||
});
|
||||
@@ -91,15 +92,16 @@ struct ConfirmEmail {
|
||||
)]
|
||||
#[post("/register/confirm")]
|
||||
async fn confirm_email_registration(
|
||||
state: web::Data<AppState>,
|
||||
request: web::Json<ConfirmEmail>,
|
||||
req: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let token = &request.token;
|
||||
|
||||
let email_token = match EmailToken::get(token).await {
|
||||
let email_token = match EmailToken::get(&state, token).await {
|
||||
Ok(password_reset) => {
|
||||
if let Err(err) = EmailToken::delete(&password_reset.token).await {
|
||||
if let Err(err) = EmailToken::delete(&state, &password_reset.token).await {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
password_reset
|
||||
@@ -109,7 +111,7 @@ async fn confirm_email_registration(
|
||||
}
|
||||
};
|
||||
|
||||
match User::select_by_email(&email_token.email).await {
|
||||
match User::select_by_email(&state.pool, &email_token.email).await {
|
||||
Some(user) => {
|
||||
let update_user = UpdateUser {
|
||||
email: None,
|
||||
@@ -121,7 +123,7 @@ async fn confirm_email_registration(
|
||||
avatar: None,
|
||||
};
|
||||
|
||||
match update_user.update(&user.username).await {
|
||||
match update_user.update(&state.pool, &user.username).await {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
log::info!(
|
||||
@@ -157,13 +159,13 @@ async fn confirm_email_registration(
|
||||
)
|
||||
)]
|
||||
#[post("/register/email")]
|
||||
async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
async fn resend_email_verification(state: web::Data<AppState>, req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let email = auth.user.email;
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
match email {
|
||||
Some(email) => {
|
||||
let user = match User::select_by_email(&email).await {
|
||||
let user = match User::select_by_email(&state.pool, &email).await {
|
||||
Some(query_user) => query_user,
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
@@ -174,11 +176,10 @@ async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse
|
||||
}
|
||||
|
||||
// Send reverify confirmation email
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = send_confirm_email(&email, &ip_address).await {
|
||||
if let Err(err) = send_confirm_email(&state, &email, &ip_address).await {
|
||||
log::error!("Failed to send reverify confirmation email: {}", err);
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
};
|
||||
});
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
@@ -195,11 +196,11 @@ async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse
|
||||
),
|
||||
)]
|
||||
#[post("/login")]
|
||||
async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
||||
async fn login(state: web::Data<AppState>, request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let username = &request.username;
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
let query_user = match User::select(&username).await {
|
||||
let query_user = match User::select(&state.pool, &username).await {
|
||||
Some(query_user) => query_user,
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
@@ -210,7 +211,7 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
let session_cookie = session.cookie();
|
||||
let session_exp_cookie = session.expiration_cookie();
|
||||
// Save the session to the database
|
||||
if let Err(err) = session.store().await {
|
||||
if let Err(err) = session.store(&state).await {
|
||||
log::error!(
|
||||
"Login attempt failure [User: {}] [IP Address: {}]: {}",
|
||||
username,
|
||||
@@ -253,14 +254,14 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
)
|
||||
)]
|
||||
#[post("/logout")]
|
||||
async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
async fn logout(state: web::Data<AppState>, req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let username = auth.user.username;
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
// Delete the session from the store
|
||||
match req.cookie(SESSION_COOKIE_NAME) {
|
||||
Some(cookie) => {
|
||||
let session_id = cookie.value().to_string();
|
||||
if let Err(err) = Session::delete(&session_id).await {
|
||||
if let Err(err) = Session::delete(&state, &session_id).await {
|
||||
log::error!(
|
||||
"Logout attempt failure [User: {}] [IP Address: {}]: {}",
|
||||
username,
|
||||
@@ -302,14 +303,14 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[get("/profile")]
|
||||
async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
async fn get_profile(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
// Verify a session cookie exists
|
||||
match req.cookie(SESSION_COOKIE_NAME) {
|
||||
// Validate the session
|
||||
Some(cookie) => {
|
||||
let session_id = cookie.value().to_string();
|
||||
let session = match Session::get(&session_id).await {
|
||||
let session = match Session::get(&state, &session_id).await {
|
||||
Ok(session) => session,
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
@@ -324,7 +325,7 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
let username = &session.username;
|
||||
let query_user = match User::select(&username).await {
|
||||
let query_user = match User::select(&state.pool, &username).await {
|
||||
Some(query_user) => query_user,
|
||||
None => {
|
||||
return HttpResponse::Unauthorized()
|
||||
@@ -366,14 +367,14 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[post("/session")]
|
||||
async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
async fn session_refresh(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
// Verify a session cookie exists
|
||||
match req.cookie(SESSION_COOKIE_NAME) {
|
||||
// Validate the session
|
||||
Some(cookie) => {
|
||||
let session_id = cookie.value().to_string();
|
||||
let session = match Session::replace(&session_id, &ip_address).await {
|
||||
let session = match Session::replace(&state, &session_id, &ip_address).await {
|
||||
Ok(session) => session,
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
@@ -428,6 +429,7 @@ struct ChangePassword {
|
||||
)]
|
||||
#[put("/password")]
|
||||
async fn change_password(
|
||||
state: web::Data<AppState>,
|
||||
request: web::Json<ChangePassword>,
|
||||
req: HttpRequest,
|
||||
auth: Auth,
|
||||
@@ -435,7 +437,7 @@ async fn change_password(
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let username = auth.user.username;
|
||||
|
||||
if let None = User::select(&username).await {
|
||||
if let None = User::select(&state.pool, &username).await {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
};
|
||||
|
||||
@@ -449,7 +451,7 @@ async fn change_password(
|
||||
avatar: None,
|
||||
};
|
||||
|
||||
match update_user.update(&username).await {
|
||||
match update_user.update(&state.pool, &username).await {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
log::info!(
|
||||
@@ -486,18 +488,18 @@ struct PasswordReset {
|
||||
)
|
||||
)]
|
||||
#[post("/password/reset")]
|
||||
async fn reset_password(request: web::Json<PasswordReset>, req: HttpRequest) -> HttpResponse {
|
||||
async fn reset_password(state: web::Data<AppState>, request: web::Json<PasswordReset>, req: HttpRequest) -> HttpResponse {
|
||||
let email = &request.email;
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let token = csprng(128);
|
||||
|
||||
// Silently return if the user's email does not exist
|
||||
if let None = User::select_by_email(&email).await {
|
||||
if let None = User::select_by_email(&state.pool, &email).await {
|
||||
return HttpResponse::Ok().finish();
|
||||
};
|
||||
|
||||
let email_token = EmailToken::new(email.clone(), token, &ip_address);
|
||||
if let Err(err) = email_token.store(86400).await {
|
||||
if let Err(err) = email_token.store(&state, 86400).await {
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
|
||||
@@ -525,6 +527,7 @@ struct ConfirmPasswordReset {
|
||||
)]
|
||||
#[post("/password/reset/confirm")]
|
||||
async fn confirm_password_reset(
|
||||
state: web::Data<AppState>,
|
||||
request: web::Json<ConfirmPasswordReset>,
|
||||
req: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
@@ -532,9 +535,9 @@ async fn confirm_password_reset(
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let token = &request.token;
|
||||
|
||||
let email_token = match EmailToken::get(token).await {
|
||||
let email_token = match EmailToken::get(&state, token).await {
|
||||
Ok(password_reset) => {
|
||||
if let Err(err) = EmailToken::delete(&password_reset.token).await {
|
||||
if let Err(err) = EmailToken::delete(&state, &password_reset.token).await {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
password_reset
|
||||
@@ -558,9 +561,9 @@ async fn confirm_password_reset(
|
||||
)
|
||||
)]
|
||||
#[get("/profile/favorites")]
|
||||
async fn get_favorites(auth: Auth) -> HttpResponse {
|
||||
async fn get_favorites(state: web::Data<AppState>, auth: Auth) -> HttpResponse {
|
||||
let username = auth.user.username;
|
||||
match UserFavorite::select_all(&username).await {
|
||||
match UserFavorite::select_all(&state.pool, &username).await {
|
||||
Ok(favorites) => HttpResponse::Ok().json(favorites),
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
}
|
||||
@@ -577,9 +580,9 @@ async fn get_favorites(auth: Auth) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[post("/profile/favorites/{icao}")]
|
||||
async fn add_favorite(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
async fn add_favorite(state: web::Data<AppState>, icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
let username = auth.user.username;
|
||||
match UserFavorite::insert(&username, &icao.into_inner()).await {
|
||||
match UserFavorite::insert(&state.pool, &username, &icao.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
}
|
||||
@@ -596,9 +599,9 @@ async fn add_favorite(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[delete("/profile/favorites/{icao}")]
|
||||
async fn remove_favorite(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
async fn remove_favorite(state: web::Data<AppState>, icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
let username = auth.user.username;
|
||||
match UserFavorite::delete(&username, &icao.into_inner()).await {
|
||||
match UserFavorite::delete(&state.pool, &username, &icao.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use super::{csprng, hash, verify_hash};
|
||||
use crate::{
|
||||
db::redis_async_connection,
|
||||
error::{ApiResult, Error},
|
||||
};
|
||||
use crate::error::{ApiResult, Error};
|
||||
use actix_web::cookie::{Cookie, time::Duration};
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use redis::aio::ConnectionManager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use crate::state::AppState;
|
||||
|
||||
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||
@@ -40,16 +39,15 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store(&self) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
pub async fn store(&self, state: &AppState) -> ApiResult<()> {
|
||||
let key = self.session_id.clone();
|
||||
let value = serde_json::to_string(self)?;
|
||||
let result: RedisResult<()> = match self.expires_at {
|
||||
let result: ApiResult<()> = match self.expires_at {
|
||||
Some(expires_at) => {
|
||||
let ttl = expires_at.timestamp() - Utc::now().timestamp();
|
||||
conn.set_ex(key, &value, ttl as u64).await
|
||||
state.set_ex(&key, &value, ttl as u64).await
|
||||
}
|
||||
None => conn.set(key, value).await,
|
||||
None => state.set(&key, &value).await,
|
||||
};
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -57,43 +55,33 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(session_id: &str) -> ApiResult<Self> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<Option<String>> = conn.get(session_id).await;
|
||||
pub async fn get(state: &AppState, session_id: &str) -> ApiResult<Self> {
|
||||
let result: Option<String> = state.get(session_id).await?;
|
||||
match result {
|
||||
Ok(Some(value)) => Ok(serde_json::from_str(&value)?),
|
||||
Ok(None) => Err(Error::new(401, format!("Missing session {}", session_id))),
|
||||
Err(err) => Err(err.into()),
|
||||
Some(value) => Ok(serde_json::from_str(&value)?),
|
||||
None => Err(Error::new(401, format!("Missing session {}", session_id))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn replace(session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
let mut session = Self::verify(session_id, ip_address).await?;
|
||||
pub async fn replace(state: &AppState, session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
let mut session = Self::verify(state, session_id, ip_address).await?;
|
||||
let session_id_owned = session_id.to_owned();
|
||||
task::spawn(async move {
|
||||
if let Err(err) = Self::delete(&session_id_owned).await {
|
||||
log::error!(
|
||||
"Error deleting old session in replace session call: {}",
|
||||
err
|
||||
);
|
||||
};
|
||||
});
|
||||
Self::delete(state, &session_id_owned).await?;
|
||||
session = Session::default(&session.username, ip_address);
|
||||
session.store().await?;
|
||||
session.store(state).await?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub async fn delete(session_id: &str) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<()> = conn.del(session_id).await;
|
||||
pub async fn delete(state: &AppState, session_id: &str) -> ApiResult<()> {
|
||||
let result: ApiResult<()> = state.del(session_id).await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify(session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
let session = Self::get(session_id).await?;
|
||||
pub async fn verify(state: &AppState, session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
let session = Self::get(state, session_id).await?;
|
||||
|
||||
// Check if the IP Address matches the Session's IP Address
|
||||
if verify_hash(ip_address, &session.ip_address) {
|
||||
@@ -103,7 +91,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> Cookie {
|
||||
pub fn cookie(&self) -> Cookie<'_> {
|
||||
let expires_at = match self.expires_at {
|
||||
Some(expires_at) => expires_at.timestamp(),
|
||||
None => DEFAULT_SESSION_TTL,
|
||||
@@ -131,7 +119,7 @@ impl Session {
|
||||
cookie
|
||||
}
|
||||
|
||||
pub fn expiration_cookie(&self) -> Cookie {
|
||||
pub fn expiration_cookie(&self) -> Cookie<'_> {
|
||||
let expires_at = match self.expires_at {
|
||||
Some(expires_at) => expires_at.timestamp(),
|
||||
None => DEFAULT_SESSION_TTL,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::db;
|
||||
use crate::{account::hash, error::ApiResult};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[allow(unused_imports)] // Import is used in schema examples
|
||||
use serde_json::json;
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const ADMIN_ROLE: &str = "ADMIN";
|
||||
@@ -107,9 +106,7 @@ pub struct UpdateUser {
|
||||
}
|
||||
|
||||
impl UpdateUser {
|
||||
pub async fn update(&self, username: &str) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn update(&self, pool: &Pool<Postgres>, username: &str) -> ApiResult<User> {
|
||||
let mut query_builder: QueryBuilder<Postgres> =
|
||||
QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME));
|
||||
|
||||
@@ -189,8 +186,7 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn select(username: &str) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
pub async fn select(pool: &Pool<Postgres>, username: &str) -> Option<Self> {
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE username = $1
|
||||
@@ -208,8 +204,7 @@ impl User {
|
||||
user
|
||||
}
|
||||
|
||||
pub async fn select_by_email(email: &str) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
pub async fn select_by_email(pool: &Pool<Postgres>, email: &str) -> Option<Self> {
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE email = $1
|
||||
@@ -228,9 +223,7 @@ impl User {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn count() -> i64 {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn count(pool: &Pool<Postgres>) -> i64 {
|
||||
sqlx::query_scalar(&format!(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM {}
|
||||
@@ -242,8 +235,7 @@ impl User {
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<User> {
|
||||
let user: User = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db;
|
||||
use crate::error::ApiResult;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
const TABLE_NAME: &str = "user_airport_favorites";
|
||||
|
||||
@@ -11,8 +11,7 @@ pub struct UserFavorite {
|
||||
}
|
||||
|
||||
impl UserFavorite {
|
||||
pub async fn select_all(username: &str) -> ApiResult<Vec<String>> {
|
||||
let pool = db::pool();
|
||||
pub async fn select_all(pool: &Pool<Postgres>, username: &str) -> ApiResult<Vec<String>> {
|
||||
let user_favorites: Vec<UserFavorite> = sqlx::query_as::<_, UserFavorite>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE username = $1
|
||||
@@ -28,8 +27,7 @@ impl UserFavorite {
|
||||
Ok(favorites)
|
||||
}
|
||||
|
||||
pub async fn insert(username: &str, icao: &str) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
pub async fn insert(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
@@ -46,8 +44,7 @@ impl UserFavorite {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(username: &str, icao: &str) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
pub async fn delete(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
DELETE FROM {} WHERE username = $1 AND icao = $2
|
||||
|
||||
@@ -2,16 +2,16 @@ use crate::airports::{
|
||||
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
|
||||
UpdateRunway,
|
||||
};
|
||||
use crate::db;
|
||||
use crate::error::{ApiResult, Error};
|
||||
use crate::metars::Metar;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures_util::try_join;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Execute, Postgres, QueryBuilder};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use crate::state::AppState;
|
||||
|
||||
const TABLE_NAME: &str = "airports";
|
||||
const DEFAULT_COLUMNS: &str = "icao, iata, local, name, category, iso_country, \
|
||||
@@ -255,8 +255,7 @@ impl From<AirportRow> for Airport {
|
||||
}
|
||||
|
||||
impl Airport {
|
||||
pub async fn select(icao: &str, metar: bool) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
pub async fn select(pool: &Pool<Postgres>, icao: &str, metar: bool) -> Option<Self> {
|
||||
|
||||
let airport_fut = async {
|
||||
sqlx::query_as(&format!(
|
||||
@@ -270,7 +269,7 @@ impl Airport {
|
||||
|
||||
let metar_fut = async {
|
||||
if metar {
|
||||
match Metar::get_all_distinct(&vec![icao.to_uppercase()]).await {
|
||||
match Metar::get_all_distinct(pool, &vec![icao.to_uppercase()]).await {
|
||||
Ok(m) => Some(m.into_iter().nth(0)),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
@@ -282,8 +281,8 @@ impl Airport {
|
||||
}
|
||||
};
|
||||
|
||||
let runways_fut = Runway::select_all(icao);
|
||||
let communications_fut = Communication::select_all(icao);
|
||||
let runways_fut = Runway::select_all(pool, icao);
|
||||
let communications_fut = Communication::select_all(pool, icao);
|
||||
|
||||
let (airport_result, runways_result, communications_result, metar_result) =
|
||||
tokio::join!(airport_fut, runways_fut, communications_fut, metar_fut);
|
||||
@@ -333,9 +332,7 @@ impl Airport {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn select_all(pool: &Pool<Postgres>, query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
||||
let mut builder =
|
||||
QueryBuilder::<Postgres>::new(format!("SELECT {} FROM {}", DEFAULT_COLUMNS, TABLE_NAME));
|
||||
|
||||
@@ -410,10 +407,10 @@ impl Airport {
|
||||
// Bulk update airport subfields
|
||||
let icaos: Vec<String> = airports.iter().map(|a| a.icao.to_uppercase()).collect();
|
||||
|
||||
let runway_future = Runway::select_all_map(&icaos);
|
||||
let frequency_future = Communication::select_all_map(&icaos);
|
||||
let runway_future = Runway::select_all_map(pool, &icaos);
|
||||
let frequency_future = Communication::select_all_map(pool, &icaos);
|
||||
let metar_future = if query.metars.unwrap_or(false) {
|
||||
Some(Metar::get_all_distinct(&icaos))
|
||||
Some(Metar::get_all_distinct(pool, &icaos))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -453,9 +450,7 @@ impl Airport {
|
||||
Ok(airports)
|
||||
}
|
||||
|
||||
pub async fn count(query: &AirportQuery) -> i64 {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn count(pool: &Pool<Postgres>, query: &AirportQuery) -> i64 {
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT COUNT(*) FROM ");
|
||||
builder.push(TABLE_NAME);
|
||||
|
||||
@@ -492,9 +487,7 @@ impl Airport {
|
||||
sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<Self> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<Self> {
|
||||
let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
|
||||
let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new();
|
||||
for runway in &self.runways {
|
||||
@@ -503,8 +496,8 @@ impl Airport {
|
||||
for frequency in &self.communications {
|
||||
all_frequency_rows.push(Communication::into(frequency, &self.icao));
|
||||
}
|
||||
Runway::insert_all(&all_runway_rows).await?;
|
||||
Communication::insert_all(&all_frequency_rows).await?;
|
||||
Runway::insert_all(pool, &all_runway_rows).await?;
|
||||
Communication::insert_all(pool, &all_frequency_rows).await?;
|
||||
|
||||
let airport: AirportRow = sqlx::query_as(&format!(
|
||||
r#"
|
||||
@@ -542,8 +535,7 @@ impl Airport {
|
||||
Ok(airport.into())
|
||||
}
|
||||
|
||||
pub async fn insert_all(airports: Vec<Self>) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
pub async fn insert_all(pool: &Pool<Postgres>, airports: Vec<Self>) -> ApiResult<()> {
|
||||
let chunk_size = 1000;
|
||||
let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
|
||||
let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new();
|
||||
@@ -593,16 +585,14 @@ impl Airport {
|
||||
query.execute(pool).await?;
|
||||
}
|
||||
|
||||
Runway::insert_all(&all_runway_rows).await?;
|
||||
Communication::insert_all(&all_frequency_rows).await?;
|
||||
Runway::insert_all(pool, &all_runway_rows).await?;
|
||||
Communication::insert_all(pool, &all_frequency_rows).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn update(pool: &Pool<Postgres>, icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
|
||||
let mut query_builder: QueryBuilder<Postgres> =
|
||||
QueryBuilder::new(format!("UPDATE {} SET ", TABLE_NAME));
|
||||
if let Some(latest_metar_observation) = airport.latest_metar_observation {
|
||||
@@ -617,9 +607,7 @@ impl Airport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(icao: &str) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn delete(pool: &Pool<Postgres>, icao: &str) -> ApiResult<()> {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
DELETE FROM {} WHERE icao = $1
|
||||
@@ -633,9 +621,7 @@ impl Airport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all() -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn delete_all(pool: &Pool<Postgres>) -> ApiResult<()> {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
DELETE FROM {} WHERE true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::db;
|
||||
use crate::error::ApiResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
@@ -65,9 +64,7 @@ impl Communication {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_all_map(icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
||||
let frequency_rows: Vec<CommunicationRow> = sqlx::query_as(&format!(
|
||||
r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
|
||||
TABLE_NAME
|
||||
@@ -89,9 +86,7 @@ impl Communication {
|
||||
Ok(frequency_map)
|
||||
}
|
||||
|
||||
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
|
||||
let frequency_row: Vec<CommunicationRow> = sqlx::query_as(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE icao = $1
|
||||
@@ -104,8 +99,7 @@ impl Communication {
|
||||
Ok(frequency_row.into_iter().map(From::from).collect())
|
||||
}
|
||||
|
||||
pub async fn insert_all(communications: &Vec<CommunicationRow>) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
pub async fn insert_all(pool: &Pool<Postgres>, communications: &Vec<CommunicationRow>) -> ApiResult<()> {
|
||||
let chunk_size = 1000;
|
||||
|
||||
for chunk in communications.chunks(chunk_size) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::db;
|
||||
use crate::error::ApiResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
@@ -64,9 +63,7 @@ impl Runway {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_all_map(icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
||||
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
|
||||
r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
|
||||
TABLE_NAME
|
||||
@@ -85,9 +82,7 @@ impl Runway {
|
||||
Ok(runway_map)
|
||||
}
|
||||
|
||||
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
|
||||
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
|
||||
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE icao = $1
|
||||
@@ -100,8 +95,7 @@ impl Runway {
|
||||
Ok(runway_rows.into_iter().map(From::from).collect())
|
||||
}
|
||||
|
||||
pub async fn insert_all(runways: &Vec<RunwayRow>) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
pub async fn insert_all(pool: &Pool<Postgres>, runways: &Vec<RunwayRow>) -> ApiResult<()> {
|
||||
let chunk_size = 1000;
|
||||
|
||||
for chunk in runways.chunks(chunk_size) {
|
||||
|
||||
@@ -5,13 +5,14 @@ use crate::airports::{AirportQuery, UpdateAirport};
|
||||
use crate::{
|
||||
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;
|
||||
use crate::state::AppState;
|
||||
use crate::utils::Paged;
|
||||
|
||||
#[derive(ToSchema)]
|
||||
#[allow(unused)]
|
||||
@@ -34,7 +35,7 @@ struct FileUpload {
|
||||
)
|
||||
)]
|
||||
#[post("/import")]
|
||||
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
async fn import_airports(state: web::Data<AppState>, mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
@@ -67,7 +68,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
|
||||
match Airport::insert_all(airports).await {
|
||||
match Airport::insert_all(&state.pool, airports).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
@@ -85,7 +86,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
),
|
||||
)]
|
||||
#[get("")]
|
||||
async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
async fn get_airports(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||
Ok(q) => q.into_inner(),
|
||||
Err(err) => {
|
||||
@@ -94,7 +95,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
|
||||
let total = Airport::count(&query).await;
|
||||
let total = Airport::count(&state.pool, &query).await;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let mut limit = query.limit.unwrap_or(total as u32);
|
||||
if limit > 1000 {
|
||||
@@ -103,7 +104,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
query.limit = Some(limit);
|
||||
query.page = Some(page);
|
||||
|
||||
match Airport::select_all(&query).await {
|
||||
match Airport::select_all(&state.pool, &query).await {
|
||||
Ok(airports) => HttpResponse::Ok().json(Paged {
|
||||
data: airports,
|
||||
page,
|
||||
@@ -125,7 +126,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
),
|
||||
)]
|
||||
#[get("/{icao}")]
|
||||
async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse {
|
||||
async fn get_airport(state: web::Data<AppState>, icao: web::Path<String>, req: HttpRequest) -> HttpResponse {
|
||||
let metar = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||
Ok(q) => q.metars.unwrap_or_else(|| false),
|
||||
Err(err) => {
|
||||
@@ -134,7 +135,7 @@ async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse
|
||||
}
|
||||
};
|
||||
|
||||
match Airport::select(&icao.into_inner(), metar).await {
|
||||
match Airport::select(&state.pool, &icao.into_inner(), metar).await {
|
||||
Some(airport) => HttpResponse::Ok().json(airport),
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
}
|
||||
@@ -152,12 +153,12 @@ async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse
|
||||
)
|
||||
)]
|
||||
#[post("")]
|
||||
async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
||||
async fn insert_airport(state: web::Data<AppState>, airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
match airport.insert().await {
|
||||
match airport.insert(&state.pool).await {
|
||||
Ok(a) => HttpResponse::Ok().json(a),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
@@ -178,6 +179,7 @@ async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse
|
||||
)]
|
||||
#[put("/{icao}")]
|
||||
async fn update_airport(
|
||||
state: web::Data<AppState>,
|
||||
icao: web::Path<String>,
|
||||
airport: web::Json<UpdateAirport>,
|
||||
auth: Auth,
|
||||
@@ -186,7 +188,7 @@ async fn update_airport(
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
match Airport::update(&icao.into_inner(), &airport.into_inner()).await {
|
||||
match Airport::update(&state.pool, &icao.into_inner(), &airport.into_inner()).await {
|
||||
Ok(a) => HttpResponse::Ok().json(a),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
@@ -206,12 +208,12 @@ async fn update_airport(
|
||||
)
|
||||
)]
|
||||
#[delete("")]
|
||||
async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||
async fn delete_airports(state: web::Data<AppState>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
match Airport::delete_all().await {
|
||||
match Airport::delete_all(&state.pool).await {
|
||||
Ok(_) => HttpResponse::NoContent().finish(),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
@@ -231,12 +233,12 @@ async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[delete("/{icao}")]
|
||||
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
async fn delete_airport(state: web::Data<AppState>, icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
match Airport::delete(&icao.into_inner()).await {
|
||||
match Airport::delete(&state.pool, &icao.into_inner()).await {
|
||||
Ok(_) => HttpResponse::NoContent().finish(),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
use crate::error::ApiResult;
|
||||
use redis::{Client as RedisClient, RedisResult, aio::MultiplexedConnection as RedisConnection};
|
||||
use s3::{Bucket, BucketConfiguration, Region, creds::Credentials, request::ResponseData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
|
||||
static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
||||
static BUCKET: OnceLock<Bucket> = OnceLock::new();
|
||||
|
||||
pub async fn initialize() -> ApiResult<()> {
|
||||
// Setup Postgres pool connection
|
||||
let pool = {
|
||||
let user = std::env::var("POSTGRES_USER").unwrap_or("aviation".to_string());
|
||||
let password = std::env::var("POSTGRES_PASSWORD").expect("POSTGRES_PASSWORD must be set");
|
||||
let host: String = std::env::var("POSTGRES_HOST").expect("POSTGRES_HOST must be set");
|
||||
let port = std::env::var("POSTGRES_PORT").unwrap_or("5432".to_string());
|
||||
let name = std::env::var("POSTGRES_DB").unwrap_or("aviation_db".to_string());
|
||||
|
||||
let db_url = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
&user, &password, &host, &port, &name
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
|
||||
&user,
|
||||
&host,
|
||||
&port,
|
||||
&name
|
||||
);
|
||||
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(30))
|
||||
.connect(&db_url)
|
||||
.await?
|
||||
};
|
||||
match POOL.set(pool) {
|
||||
Ok(_) => log::info!("Database connection established"),
|
||||
Err(_) => log::warn!("Database pool already initialized"),
|
||||
}
|
||||
|
||||
// Setup Redis connection
|
||||
let redis = {
|
||||
let host = std::env::var("REDIS_HOST").unwrap_or("localhost".to_string());
|
||||
let port = std::env::var("REDIS_PORT").unwrap_or("6379".to_string());
|
||||
let url = format!("redis://{}:{}", host, port);
|
||||
log::info!("Connecting to redis at {}", &url);
|
||||
RedisClient::open(url).expect("Failed to create redis client")
|
||||
};
|
||||
match REDIS.set(redis) {
|
||||
Ok(_) => log::info!("Redis connection established"),
|
||||
Err(_) => log::warn!("Redis client already initialized"),
|
||||
}
|
||||
|
||||
// Setup Bucket connection
|
||||
let bucket = {
|
||||
let protocol = std::env::var("MINIO_PROTOCOL").unwrap_or("http".to_string());
|
||||
let host = std::env::var("MINIO_HOST").unwrap_or("localhost".to_string());
|
||||
let port = std::env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
||||
let user = std::env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
||||
let password = std::env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set");
|
||||
let bucket_name = std::env::var("MINIO_BUCKET").unwrap_or("aviation".to_string());
|
||||
let url = format!("{}://{}:{}", protocol, host, port);
|
||||
|
||||
let region = Region::Custom {
|
||||
region: "".to_string(),
|
||||
endpoint: url.to_string(),
|
||||
};
|
||||
|
||||
let credentials = Credentials {
|
||||
access_key: Some(user),
|
||||
secret_key: Some(password),
|
||||
security_token: None,
|
||||
session_token: None,
|
||||
expiration: None,
|
||||
};
|
||||
|
||||
let bucket = Bucket::new(&bucket_name, region.clone(), credentials.clone())?.with_path_style();
|
||||
log::info!("Checking for object in bucket at {}", ®ion.endpoint());
|
||||
match bucket.head_object("/").await {
|
||||
Ok(_) => bucket,
|
||||
Err(_) => {
|
||||
log::debug!("Creating '{}' bucket", &bucket_name);
|
||||
let response = match Bucket::create_with_path_style(
|
||||
&bucket_name,
|
||||
region,
|
||||
credentials,
|
||||
BucketConfiguration::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
log::error!("Failed to create bucket '{}': {}", &bucket_name, err);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
response.bucket
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match BUCKET.set(*bucket) {
|
||||
Ok(_) => log::info!("Bucket connection initialized"),
|
||||
Err(_) => log::warn!("Bucket connection already initialized"),
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
match run_migrations().await {
|
||||
Ok(_) => log::debug!("Successfully ran database migrations"),
|
||||
Err(e) => log::error!("Failed to run migrations: {}", e),
|
||||
}
|
||||
|
||||
log::info!("Database initialized");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pool() -> &'static Pool<Postgres> {
|
||||
POOL.get().unwrap()
|
||||
}
|
||||
|
||||
fn redis() -> &'static RedisClient {
|
||||
REDIS.get().unwrap()
|
||||
}
|
||||
|
||||
// pub fn redis_connection() -> RedisResult<redis::Connection> {
|
||||
// let conn = redis().get_connection()?;
|
||||
// Ok(conn)
|
||||
// }
|
||||
|
||||
pub async fn redis_async_connection() -> RedisResult<RedisConnection> {
|
||||
let conn = redis().get_multiplexed_async_connection().await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
async fn run_migrations() -> ApiResult<()> {
|
||||
log::debug!("Running database migrations");
|
||||
let pool = pool();
|
||||
sqlx::migrate!().run(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upload_file(path: &str, content: &[u8]) -> ApiResult<ResponseData> {
|
||||
let response = BUCKET.get().unwrap().put_object(path, content).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_file(path: &str) -> ApiResult<Vec<u8>> {
|
||||
let response = BUCKET.get().unwrap().get_object(path).await?;
|
||||
let bytes = response.bytes();
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
pub async fn delete_file(path: &str) -> ApiResult<ResponseData> {
|
||||
let response = BUCKET.get().unwrap().delete_object(path).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Paged<T> {
|
||||
pub data: T,
|
||||
pub page: u32,
|
||||
pub limit: u32,
|
||||
pub total: i64,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
use std::sync::{MutexGuard, PoisonError};
|
||||
|
||||
pub type ApiResult<T> = Result<T, Error>;
|
||||
|
||||
@@ -15,10 +16,10 @@ pub struct Error {
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(status: u16, message: String) -> Self {
|
||||
pub fn new(status: u16, details: String) -> Self {
|
||||
Self {
|
||||
status,
|
||||
details: message,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,3 +237,9 @@ impl From<regex::Error> for Error {
|
||||
Self::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<PoisonError<MutexGuard<'a, T>>> for Error {
|
||||
fn from(_: PoisonError<MutexGuard<'a, T>>) -> Self {
|
||||
Self::new(500, "Failed to acquire lock".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::account::User;
|
||||
use crate::account::{ADMIN_ROLE, hash};
|
||||
use crate::http_client::HttpClient;
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||
use dotenv::from_filename;
|
||||
@@ -10,27 +9,24 @@ use utoipa::openapi::SecurityRequirement;
|
||||
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||
use utoipa_actix_web::{AppExt, scope};
|
||||
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||
use crate::state::AppState;
|
||||
|
||||
mod account;
|
||||
mod airports;
|
||||
mod db;
|
||||
mod error;
|
||||
mod http_client;
|
||||
mod metars;
|
||||
mod scheduler;
|
||||
mod smtp;
|
||||
mod system;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
client: Arc<HttpClient>,
|
||||
}
|
||||
mod state;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
initialize_environment()?;
|
||||
db::initialize().await?;
|
||||
scheduler::run();
|
||||
let state = Arc::new(AppState::new().await?);
|
||||
scheduler::run(state.clone());
|
||||
|
||||
// Initialize admin user
|
||||
let admin_username = env::var("ADMIN_USERNAME");
|
||||
@@ -39,7 +35,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if admin_username.is_ok() && admin_email.is_ok() && admin_password.is_ok() {
|
||||
let username = admin_username.unwrap();
|
||||
let email = admin_email.unwrap();
|
||||
if User::select_by_email(&email).await.is_none() {
|
||||
if User::select_by_email(&state.pool, &email).await.is_none() {
|
||||
log::debug!("Creating default administrator");
|
||||
let password = admin_password.unwrap();
|
||||
let password_hash = hash(&password)?;
|
||||
@@ -60,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
updated_at: Default::default(),
|
||||
created_at: Default::default(),
|
||||
};
|
||||
match admin_user.insert().await {
|
||||
match admin_user.insert(&state.pool).await {
|
||||
Ok(_) => log::debug!("Default administrator was successfully created"),
|
||||
Err(err) => {
|
||||
log::warn!("{}", err);
|
||||
@@ -69,8 +65,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
let client = Arc::new(HttpClient::default()?);
|
||||
let state = AppState { client };
|
||||
let host = "0.0.0.0";
|
||||
let port = env::var("API_PORT").unwrap_or("5000".to_string());
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::db::redis_async_connection;
|
||||
use crate::error::ApiResult;
|
||||
use crate::metars::Metar;
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MetarCheck {
|
||||
@@ -14,8 +13,8 @@ pub struct MetarCheck {
|
||||
}
|
||||
|
||||
impl MetarCheck {
|
||||
pub async fn new(icao: String, status: bool) -> Self {
|
||||
match Self::get(&icao).await {
|
||||
pub async fn new(state: &AppState, icao: String, status: bool) -> Self {
|
||||
match Self::get(state, &icao).await {
|
||||
Some(c) => Self {
|
||||
icao,
|
||||
status,
|
||||
@@ -31,15 +30,8 @@ impl MetarCheck {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(icao: &str) -> Option<MetarCheck> {
|
||||
let mut conn = match redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
log::error!("Unable to get connection for ICAO {}: {}", icao, err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let result: RedisResult<Option<String>> = conn.get(icao).await;
|
||||
pub async fn get(state: &AppState, icao: &str) -> Option<MetarCheck> {
|
||||
let result: ApiResult<Option<String>> = state.get(icao).await;
|
||||
match result {
|
||||
Ok(Some(value)) => match serde_json::from_str(&value) {
|
||||
Ok(result) => Some(result),
|
||||
@@ -56,10 +48,9 @@ impl MetarCheck {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
pub async fn insert(&self, state: &AppState) -> ApiResult<()> {
|
||||
let value = serde_json::to_string(&self)?;
|
||||
conn.set::<_, _, ()>(self.icao.as_str(), value).await?;
|
||||
state.set(self.icao.as_str(), &value).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use crate::airports::{Airport, UpdateAirport};
|
||||
use crate::error::Error;
|
||||
use crate::http_client::HttpClient;
|
||||
use crate::metars::MetarCheck;
|
||||
use crate::metars::utils::parse_metar_time;
|
||||
use crate::{db, error::ApiResult};
|
||||
use crate::error::ApiResult;
|
||||
use chrono::{DateTime, Utc};
|
||||
use flate2::read::GzDecoder;
|
||||
use reqwest::header::ETAG;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
use regex::Regex;
|
||||
use utoipa::ToSchema;
|
||||
use crate::state::AppState;
|
||||
|
||||
static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
|
||||
|
||||
@@ -278,8 +279,7 @@ struct MetarRow {
|
||||
}
|
||||
|
||||
impl MetarRow {
|
||||
async fn insert(&self) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
@@ -305,8 +305,7 @@ impl MetarRow {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_all(metars: Vec<Metar>) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
async fn insert_all(pool: &Pool<Postgres>, metars: Vec<Metar>) -> ApiResult<()> {
|
||||
let chunk_size = 1000;
|
||||
|
||||
for chunk in metars.chunks(chunk_size) {
|
||||
@@ -342,10 +341,10 @@ impl MetarRow {
|
||||
}
|
||||
|
||||
impl Metar {
|
||||
fn parse_multiple(metar_strings: &Vec<&str>) -> ApiResult<Vec<Self>> {
|
||||
fn parse_multiple(pool: &Pool<Postgres>, metar_strings: &Vec<&str>) -> ApiResult<Vec<Self>> {
|
||||
let mut metars: Vec<Self> = vec![];
|
||||
for metar_string in metar_strings {
|
||||
match Self::parse(metar_string) {
|
||||
match Self::parse(pool, metar_string) {
|
||||
Ok(metar) => metars.push(metar),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to parse metar string: {}", e);
|
||||
@@ -357,7 +356,7 @@ impl Metar {
|
||||
Ok(metars)
|
||||
}
|
||||
|
||||
fn parse(metar_string: &str) -> ApiResult<Self> {
|
||||
fn parse(pool: &Pool<Postgres>, metar_string: &str) -> ApiResult<Self> {
|
||||
if metar_string.is_empty() {
|
||||
return Err(Error::new(
|
||||
404,
|
||||
@@ -368,7 +367,11 @@ impl Metar {
|
||||
log::trace!("Parsing METAR data: {}", metar_string);
|
||||
let mut metar: Self = Self::default();
|
||||
metar.raw_text = metar_string.to_owned();
|
||||
let mut metar_parts: Vec<&str> = metar_string.split_whitespace().collect();
|
||||
let mut metar_parts: Vec<&str> = metar_string
|
||||
.trim()
|
||||
.trim_matches(|c| c == '"' || c == '\'' || c == '“' || c == '”' || c == '‘' || c == '’')
|
||||
.trim()
|
||||
.split_whitespace().collect();
|
||||
if metar_parts.len() < 4 {
|
||||
return Err(Error::new(
|
||||
500,
|
||||
@@ -380,8 +383,14 @@ impl Metar {
|
||||
}
|
||||
|
||||
// Remove METAR at the start of the text
|
||||
if metar_parts[0].to_string() == "METAR".to_string() {
|
||||
let metar_re: Regex = Regex::new(r"(?i)^[\p{P}\s]*METAR[\p{P}\s]*$")?;
|
||||
let speci_re: Regex = Regex::new(r"(?i)^[\p{P}\s]*SPECI[\p{P}\s]*$")?;
|
||||
let token = metar_parts[0].trim();
|
||||
|
||||
if metar_re.is_match(token) {
|
||||
metar_parts.remove(0);
|
||||
} else if speci_re.is_match(token) {
|
||||
return Err(Error::new(500, format!("Unable to parse SPECI data: {}", metar_string)));
|
||||
}
|
||||
|
||||
// Station Identifier
|
||||
@@ -880,8 +889,10 @@ impl Metar {
|
||||
// Update the airport's metar observation time
|
||||
let icao = metar.icao.clone();
|
||||
let observation_time = metar.observation_time.clone();
|
||||
let pool = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
match Airport::update(
|
||||
&pool.clone(),
|
||||
&icao,
|
||||
&UpdateAirport {
|
||||
icao: None,
|
||||
@@ -982,13 +993,13 @@ impl Metar {
|
||||
}
|
||||
|
||||
pub async fn get_cached_remote_metars(
|
||||
client: &HttpClient,
|
||||
state: &AppState,
|
||||
etag: Option<String>,
|
||||
) -> ApiResult<(Vec<Self>, String)> {
|
||||
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
|
||||
let url = format!("{}/data/cache/metars.cache.csv.gz", base_url);
|
||||
|
||||
match client.get(&url, etag.clone()).await {
|
||||
match state.client.get(&url, etag.clone()).await {
|
||||
Ok(r) => {
|
||||
let new_etag = r
|
||||
.headers()
|
||||
@@ -1006,7 +1017,7 @@ impl Metar {
|
||||
for line in text.lines() {
|
||||
// Split off the first column
|
||||
let raw_text = line.splitn(2, ',').next().unwrap();
|
||||
match Metar::parse(raw_text) {
|
||||
match Metar::parse(&state.pool, raw_text) {
|
||||
Ok(m) => output.push(m),
|
||||
Err(err) => {
|
||||
log::warn!("{}", err);
|
||||
@@ -1026,7 +1037,7 @@ impl Metar {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_remote_metars(client: &HttpClient, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
|
||||
pub async fn get_remote_metars(state: &AppState, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
|
||||
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
|
||||
// Query the remote API for the missing METAR data 10 at a time
|
||||
let icao_chunks = icaos
|
||||
@@ -1039,7 +1050,7 @@ impl Metar {
|
||||
"{}/api/data/metar?ids={}&hours=0&order=id,-obs",
|
||||
base_url, icao_chunk
|
||||
);
|
||||
let mut m = match client.get(&url, None).await {
|
||||
let mut m = match state.client.get(&url, None).await {
|
||||
Ok(r) => match r.text().await {
|
||||
Ok(r) => {
|
||||
let metar_chunk = r
|
||||
@@ -1047,7 +1058,7 @@ impl Metar {
|
||||
.split("\n")
|
||||
.filter(|m| !m.trim().is_empty())
|
||||
.collect();
|
||||
match Self::parse_multiple(&metar_chunk) {
|
||||
match Self::parse_multiple(&state.pool, &metar_chunk) {
|
||||
Ok(m) => m,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
@@ -1076,12 +1087,11 @@ impl Metar {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_all_distinct(icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
|
||||
pub async fn get_all_distinct(pool: &Pool<Postgres>, icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
|
||||
if icao_list.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let pool = db::pool();
|
||||
let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!(
|
||||
r#"
|
||||
SELECT DISTINCT ON (icao) * FROM {}
|
||||
@@ -1101,10 +1111,10 @@ impl Metar {
|
||||
}
|
||||
|
||||
pub async fn get_or_update_metars(
|
||||
client: &HttpClient,
|
||||
state: &AppState,
|
||||
icaos: &Vec<String>,
|
||||
) -> ApiResult<Vec<Self>> {
|
||||
let metars = Self::get_all_distinct(&icaos).await?;
|
||||
let metars = Self::get_all_distinct(&state.pool, &icaos).await?;
|
||||
let current_time = Utc::now().timestamp();
|
||||
|
||||
let mut updated_metars: Vec<Self> = vec![];
|
||||
@@ -1120,7 +1130,7 @@ impl Metar {
|
||||
// Handle outdated METARs
|
||||
if current_time > (metar.observation_time.timestamp() + time_offset()) {
|
||||
// If the METAR has previously been found, get the updated_at time, otherwise default
|
||||
let refresh_seconds = match MetarCheck::get(&icao).await {
|
||||
let refresh_seconds = match MetarCheck::get(state, &icao).await {
|
||||
Some(c) => current_time - c.updated_at.timestamp(),
|
||||
None => DEFAULT_REFRESH_DURATION,
|
||||
};
|
||||
@@ -1143,15 +1153,15 @@ impl Metar {
|
||||
// Otherwise add the valid metar to the updated list
|
||||
else {
|
||||
found_metar_icaos.insert(icao.clone());
|
||||
let metar_check = MetarCheck::new(icao, true).await;
|
||||
metar_check.insert().await?;
|
||||
let metar_check = MetarCheck::new(state, icao, true).await;
|
||||
metar_check.insert(state).await?;
|
||||
updated_metars.push(metar);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all METARs that were not in the returned database METARs
|
||||
for icao in &requested_icaos {
|
||||
match MetarCheck::get(icao).await {
|
||||
match MetarCheck::get(state, icao).await {
|
||||
Some(c) => {
|
||||
if current_time > (c.updated_at.timestamp() + DEFAULT_REFRESH_DURATION) {
|
||||
missing_metar_icaos.push(icao.to_string());
|
||||
@@ -1169,7 +1179,7 @@ impl Metar {
|
||||
"Retrieving missing METAR data for {:?}",
|
||||
missing_metar_icaos
|
||||
);
|
||||
let mut remote_metars = Self::get_remote_metars(client, &missing_metar_icaos)
|
||||
let mut remote_metars = Self::get_remote_metars(&state, &missing_metar_icaos)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::warn!("Unable to get remote METAR data; {}", err);
|
||||
@@ -1179,19 +1189,19 @@ impl Metar {
|
||||
// Insert missing METARs
|
||||
if remote_metars.len() > 0 {
|
||||
for remote_metar in remote_metars.clone() {
|
||||
remote_metar.insert().await?;
|
||||
remote_metar.insert(&state.pool).await?;
|
||||
found_metar_icaos.insert(remote_metar.icao.to_string());
|
||||
let mut metar_check = MetarCheck::new(remote_metar.icao.clone(), true).await;
|
||||
let mut metar_check = MetarCheck::new(state, remote_metar.icao.clone(), true).await;
|
||||
metar_check.last_metar = Some(remote_metar);
|
||||
metar_check.insert().await?;
|
||||
metar_check.insert(state).await?;
|
||||
}
|
||||
updated_metars.append(&mut remote_metars);
|
||||
}
|
||||
|
||||
// Update still missing METARs
|
||||
for difference in found_metar_icaos.symmetric_difference(&requested_icaos) {
|
||||
let metar_check = MetarCheck::new(difference.to_string(), false).await;
|
||||
metar_check.insert().await?;
|
||||
let metar_check = MetarCheck::new(state, difference.to_string(), false).await;
|
||||
metar_check.insert(state).await?;
|
||||
// Only add cached metar data if it's less than 4 hours old
|
||||
if let Some(last_metar) = metar_check.last_metar {
|
||||
let four_hours_ago = Utc::now() - chrono::Duration::hours(4);
|
||||
@@ -1205,26 +1215,26 @@ impl Metar {
|
||||
Ok(updated_metars)
|
||||
}
|
||||
|
||||
pub async fn update_metars(client: &HttpClient, etag: Option<String>) -> ApiResult<String> {
|
||||
let (remote_metars, etag) = Self::get_cached_remote_metars(client, etag)
|
||||
pub async fn update_metars(state: &AppState, etag: Option<String>) -> ApiResult<String> {
|
||||
let (remote_metars, etag) = Self::get_cached_remote_metars(state, etag)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::warn!("Unable to get cached remote METAR data; {}", err);
|
||||
(vec![], String::new())
|
||||
});
|
||||
MetarRow::insert_all(remote_metars).await?;
|
||||
MetarRow::insert_all(&state.pool, remote_metars).await?;
|
||||
|
||||
Ok(etag)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<()> {
|
||||
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
|
||||
log::trace!(
|
||||
"Inserting metar {} with observation time {}",
|
||||
self.icao,
|
||||
self.observation_time
|
||||
);
|
||||
let metar: MetarRow = self.to_row()?;
|
||||
metar.insert().await?;
|
||||
metar.insert(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1235,43 +1245,45 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metar_parse() {
|
||||
let state = AppState::new().await.unwrap();
|
||||
|
||||
let mut metar_string = "METAR KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT \
|
||||
-RA BR BKN015 OVC025 06/04 A2990 RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 \
|
||||
RWY11 RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 \
|
||||
TSNO $"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
|
||||
metar_string = "KMIA 090053Z 33004KT 10SM FEW015 FEW024 SCT075 SCT250 25/22 A2990 RMK AO2 \
|
||||
SLP126 T02500217 $"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
|
||||
metar_string =
|
||||
"KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
|
||||
metar_string = "KHEF 092356Z 13009KT 10SM CLR 08/M03 A3022 RMK AO2 SLP239 6//// T00831033 \
|
||||
10133 20078 53002 PNO $"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
|
||||
metar_string = "KSLK 162351Z AUTO VRB03KT 1SM -SN BR FEW007 OVC014 00/M02 A2974 RMK AO2 \
|
||||
SLP090 P0001 60004 T00001017 10000 21011 53026"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
|
||||
metar_string = "KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT -RA BR BKN015 OVC025 \
|
||||
SCTCB FEW123TCU 06/04 A2990 RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 RWY11 \
|
||||
RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 TSNO $"
|
||||
.to_string();
|
||||
let metar = Metar::parse(&metar_string).unwrap();
|
||||
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
|
||||
dbg!(&metar.observation_time);
|
||||
dbg!(&metar.sky_condition);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ struct MetarQuery {
|
||||
),
|
||||
)]
|
||||
#[get("")]
|
||||
async fn find_all(req: HttpRequest) -> HttpResponse {
|
||||
async fn find_all(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
||||
let icao_option = ¶meters.icaos;
|
||||
if let None = icao_option {
|
||||
@@ -37,7 +37,7 @@ async fn find_all(req: HttpRequest) -> HttpResponse {
|
||||
};
|
||||
let icaos: Vec<String> = icao_string.split(',').map(|s| s.to_uppercase()).collect();
|
||||
|
||||
let metars = match Metar::get_all_distinct(&icaos).await {
|
||||
let metars = match Metar::get_all_distinct(&state.pool, &icaos).await {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
@@ -61,8 +61,8 @@ async fn find_all(req: HttpRequest) -> HttpResponse {
|
||||
)
|
||||
)]
|
||||
#[put("")]
|
||||
async fn refresh_metars(data: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||
let client = data.client.clone();
|
||||
async fn refresh_metars(state: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||
let client = state.client.clone();
|
||||
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
||||
let icao_option = ¶meters.icaos;
|
||||
if let None = icao_option {
|
||||
@@ -75,7 +75,7 @@ async fn refresh_metars(data: web::Data<AppState>, req: HttpRequest, _auth: Auth
|
||||
};
|
||||
let icaos: Vec<String> = icao_string.split(',').map(|s| s.to_uppercase()).collect();
|
||||
|
||||
let metars = match Metar::get_or_update_metars(&client, &icaos).await {
|
||||
let metars = match Metar::get_or_update_metars(&state, &icaos).await {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use crate::http_client::HttpClient;
|
||||
use crate::metars::Metar;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::interval;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub fn run() {
|
||||
tokio::spawn(async {
|
||||
let client = match HttpClient::default() {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
log::error!("Failed to create HTTP client: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
pub fn run(state: Arc<AppState>) {
|
||||
tokio::spawn(async move {
|
||||
let seconds = env::var("METAR_INTERVAL")
|
||||
.unwrap_or("300".to_string())
|
||||
.parse::<u64>()
|
||||
@@ -32,7 +26,7 @@ pub fn run() {
|
||||
log::debug!("METAR update started at {}", start_utc);
|
||||
|
||||
// Run the update
|
||||
match Metar::update_metars(&client, etag.clone()).await {
|
||||
match Metar::update_metars(&state, etag.clone()).await {
|
||||
Ok(new_etag) => etag = Some(new_etag),
|
||||
Err(err) => log::error!("METAR update failed: {}", err),
|
||||
}
|
||||
|
||||
167
api/src/state.rs
Normal file
167
api/src/state.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::env;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use redis::aio::ConnectionManager;
|
||||
use redis::AsyncTypedCommands;
|
||||
use s3::{Bucket, BucketConfiguration, Region};
|
||||
use s3::creds::Credentials;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use crate::error::ApiResult;
|
||||
use crate::http_client::HttpClient;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub client: HttpClient,
|
||||
pub pool: Pool<Postgres>,
|
||||
pub connection_manager: Arc<Mutex<ConnectionManager>>,
|
||||
pub bucket: Box<Bucket>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub async fn new() -> ApiResult<Self> {
|
||||
let client = HttpClient::default()?;
|
||||
|
||||
let pool: Pool<Postgres> = {
|
||||
let user = env::var("POSTGRES_USER").unwrap_or("raac".to_string());
|
||||
let password = env::var("POSTGRES_PASSWORD").expect("POSTGRES_PASSWORD must be set");
|
||||
let host: String = env::var("POSTGRES_HOST").expect("POSTGRES_HOST must be set");
|
||||
let port = env::var("POSTGRES_PORT").unwrap_or("5432".to_string());
|
||||
let name = env::var("POSTGRES_DB").unwrap_or("raac".to_string());
|
||||
|
||||
let url = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
&user, &password, &host, &port, &name
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
|
||||
&user,
|
||||
&host,
|
||||
&port,
|
||||
&name
|
||||
);
|
||||
|
||||
let connections = env::var("POSTGRES_CONNECTIONS")
|
||||
.unwrap_or("5".to_string())
|
||||
.parse::<u32>()
|
||||
.unwrap_or(5);
|
||||
let timeout = env::var("POSTGRES_TIMEOUT")
|
||||
.unwrap_or("30".to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(30);
|
||||
|
||||
PgPoolOptions::new()
|
||||
.max_connections(connections)
|
||||
.acquire_timeout(Duration::from_secs(timeout))
|
||||
.connect(&url)
|
||||
.await
|
||||
.expect("Failed to create postgres pool")
|
||||
};
|
||||
|
||||
let run_migrations = env::var("POSTGRES_MIGRATE")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(true);
|
||||
|
||||
if run_migrations {
|
||||
log::debug!("Running database migrations...");
|
||||
match sqlx::migrate!().run(&pool).await {
|
||||
Ok(_) => log::debug!("Database migrations completed"),
|
||||
Err(err) => log::error!("Failed to run database migrations: {}", err),
|
||||
};
|
||||
}
|
||||
|
||||
let connection_manager: ConnectionManager = {
|
||||
let host = env::var("VALKEY_HOST").unwrap_or("localhost".to_string());
|
||||
let port = env::var("VALKEY_PORT").unwrap_or("6379".to_string());
|
||||
let url = format!("redis://{}:{}", host, port);
|
||||
log::info!("Connecting to in-memory datastore at {}...", &url);
|
||||
let client = redis::Client::open(url).expect("Failed to create in-memory datastore client");
|
||||
ConnectionManager::new(client)
|
||||
.await
|
||||
.expect("Failed to create in-memory datastore connection manager")
|
||||
};
|
||||
|
||||
// Setup Bucket connection
|
||||
let bucket = {
|
||||
let protocol = std::env::var("MINIO_PROTOCOL").unwrap_or("http".to_string());
|
||||
let host = std::env::var("MINIO_HOST").unwrap_or("localhost".to_string());
|
||||
let port = std::env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
||||
let user = std::env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
||||
let password = std::env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set");
|
||||
let bucket_name = std::env::var("MINIO_BUCKET").unwrap_or("aviation".to_string());
|
||||
let url = format!("{}://{}:{}", protocol, host, port);
|
||||
|
||||
let region = Region::Custom {
|
||||
region: "".to_string(),
|
||||
endpoint: url.to_string(),
|
||||
};
|
||||
|
||||
let credentials = Credentials {
|
||||
access_key: Some(user),
|
||||
secret_key: Some(password),
|
||||
security_token: None,
|
||||
session_token: None,
|
||||
expiration: None,
|
||||
};
|
||||
|
||||
let bucket = Bucket::new(&bucket_name, region.clone(), credentials.clone())?.with_path_style();
|
||||
log::info!("Checking for object in bucket at {}", ®ion.endpoint());
|
||||
match bucket.head_object("/").await {
|
||||
Ok(_) => bucket,
|
||||
Err(_) => {
|
||||
log::debug!("Creating '{}' bucket", &bucket_name);
|
||||
let response = match Bucket::create_with_path_style(
|
||||
&bucket_name,
|
||||
region,
|
||||
credentials,
|
||||
BucketConfiguration::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
log::error!("Failed to create bucket '{}': {}", &bucket_name, err);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
response.bucket
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
pool,
|
||||
connection_manager: Arc::new(Mutex::new(connection_manager)),
|
||||
bucket,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set(&self, key: &str, value: &str) -> ApiResult<()> {
|
||||
let mut connection_manager = self.connection_manager.lock()?;
|
||||
connection_manager.set(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_ex(&self, key: &str, value: &str, seconds: u64) -> ApiResult<()> {
|
||||
let mut connection_manager = self.connection_manager.lock()?;
|
||||
connection_manager.set_ex(key, value, seconds).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(&self, key: &str) -> ApiResult<Option<String>> {
|
||||
let mut connection_manager = self.connection_manager.lock()?;
|
||||
match connection_manager.get(key).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn del(&self, key: &str) -> ApiResult<()> {
|
||||
let mut connection_manager = self.connection_manager.lock()?;
|
||||
connection_manager.del(key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
9
api/src/utils.rs
Normal file
9
api/src/utils.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Paged<T> {
|
||||
pub data: Vec<T>,
|
||||
pub page: u32,
|
||||
pub limit: u32,
|
||||
pub total: i64,
|
||||
}
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
volumes:
|
||||
- ./ssl:/etc/nginx/ssl/
|
||||
networks:
|
||||
- default
|
||||
- web
|
||||
<<: *default_restart
|
||||
|
||||
postgres:
|
||||
@@ -37,36 +37,32 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- /data/aviation/postgres:/var/lib/postgresql/data
|
||||
- /data/aviation/postgres_logs:/var/log
|
||||
- postgres:/var/lib/postgresql/data
|
||||
- postgres_logs:/var/log
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
networks:
|
||||
- default
|
||||
profiles:
|
||||
- backend
|
||||
<<: *default_restart
|
||||
|
||||
redis:
|
||||
image: gitea.bensherriff.com/homelab/redis:8.0-M03
|
||||
container_name: aviation-redis
|
||||
valkey:
|
||||
image: valkey/valkey:8.1.3
|
||||
container_name: aviation-valkey
|
||||
volumes:
|
||||
- redis:/data
|
||||
- valkey:/data
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
- "${VALKEY_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||
test: [ "CMD", "valkey-cli", "--raw", "incr", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
- default
|
||||
profiles:
|
||||
- backend
|
||||
<<: *default_restart
|
||||
|
||||
minio:
|
||||
image: gitea.bensherriff.com/homelab/minio:RELEASE.2025-02-28T09-55-16Z
|
||||
image: minio/minio:RELEASE.2025-07-23T15-54-02Z
|
||||
container_name: aviation-minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
@@ -78,8 +74,6 @@ services:
|
||||
ports:
|
||||
- "${MINIO_PORT:-9000}:9000"
|
||||
- "${MINIO_INTERNAL_PORT:-9001}:9001"
|
||||
networks:
|
||||
- default
|
||||
profiles:
|
||||
- backend
|
||||
command: server --console-address ":9001" /data
|
||||
@@ -97,8 +91,8 @@ services:
|
||||
API_PORT: 5000
|
||||
POSTGRES_HOST: aviation-postgres
|
||||
POSTGRES_PORT: 5432
|
||||
REDIS_HOST: aviation-redis
|
||||
REDIS_PORT: 6379
|
||||
VALKEY_HOST: aviation-valkey
|
||||
VALKEY_PORT: 6379
|
||||
MINIO_HOST: aviation-minio
|
||||
MINIO_PORT: 9000
|
||||
TEMPLATE_DIR: /templates
|
||||
@@ -109,36 +103,12 @@ services:
|
||||
- "${API_PORT:-5000}:5000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- valkey
|
||||
- minio
|
||||
networks:
|
||||
- default
|
||||
profiles:
|
||||
- api
|
||||
<<: *default_restart
|
||||
|
||||
# Development Containers
|
||||
# ui-dev:
|
||||
# image: gitea.bensherriff.com/bsherriff/aviation-ui:latest
|
||||
# container_name: aviation-ui-dev
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# env_file: *env
|
||||
# environment:
|
||||
# - VITE_NODE_ENV=${VITE_NODE_ENV:-development}
|
||||
# ports:
|
||||
# - "${UI_PORT:-3000}:3000"
|
||||
# volumes:
|
||||
# - ./ui/src:/app/src
|
||||
# - ./ui/public:/app/public
|
||||
# - ./ui/styles:/app/styles
|
||||
# networks:
|
||||
# - default
|
||||
# profiles:
|
||||
# - dev
|
||||
# command: ["npm", "run", "dev"]
|
||||
# <<: *default_restart
|
||||
mailpit:
|
||||
image: axllent/mailpit
|
||||
container_name: mailpit
|
||||
@@ -152,16 +122,17 @@ services:
|
||||
- "${MAILPIT_SMTP_PORT:-1025}:1025"
|
||||
volumes:
|
||||
- mailpit:/data
|
||||
networks:
|
||||
- default
|
||||
profiles:
|
||||
- dev
|
||||
<<: *default_restart
|
||||
|
||||
volumes:
|
||||
redis:
|
||||
postgres:
|
||||
postgres_logs:
|
||||
valkey:
|
||||
minio:
|
||||
mailpit:
|
||||
|
||||
networks:
|
||||
default:
|
||||
web:
|
||||
driver: bridge
|
||||
|
||||
@@ -4,12 +4,10 @@ worker_processes auto;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
@@ -20,12 +18,16 @@ http {
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# allow HTTP/2 on the front‐end
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
gzip on;
|
||||
|
||||
# Set client limit to 100 MB
|
||||
client_max_body_size 100M;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* Set up Flexbox layout */
|
||||
.App {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
|
||||
@@ -6,7 +6,6 @@ import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
|
||||
import markerIcon from 'leaflet/dist/images/marker-icon.png';
|
||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
import L from 'leaflet';
|
||||
import { Header } from '@components/Header';
|
||||
import AirportLayer from '@components/AirportLayer.tsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Airport } from '@lib/airport.types.ts';
|
||||
@@ -16,7 +15,6 @@ import { IconBuildingAirport, IconRadar } from '@tabler/icons-react';
|
||||
import { GroupControl } from '@components/GroupControl.tsx';
|
||||
import { AirportDrawer } from '@components/AirportDrawer';
|
||||
import { LocateControl } from '@components/LocateControl.tsx';
|
||||
import { Footer } from '@components/Footer';
|
||||
// Fix Leaflet's default icon path issues with Webpack
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
@@ -91,10 +89,9 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
<Header />
|
||||
<div className='map-wrapper'>
|
||||
<div className='map-wrapper' style={{ flex: 1, minHeight: 0 }}>
|
||||
<MapContainer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
className='leaflet-container'
|
||||
attributionControl={false}
|
||||
center={defaultCenter}
|
||||
@@ -141,8 +138,6 @@ function App() {
|
||||
/>
|
||||
</MapContainer>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Header } from '@components/Header';
|
||||
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||
import { AirportTable } from '@components/AirportTable';
|
||||
import { AirportDrop } from '@components/AirportDrop';
|
||||
@@ -13,7 +12,6 @@ export function Administration() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<AirportTable />
|
||||
<AirportDrop />
|
||||
</>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function AirportDrawer({
|
||||
onClose={() => setAirport(null)}
|
||||
withinPortal
|
||||
zIndex={1000}
|
||||
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0, backgroundColor: 'red' } }}
|
||||
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }}
|
||||
padding='md'
|
||||
size={isMobile ? '100%' : 'md'}
|
||||
position='left'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
.footer {
|
||||
background: #32495f;
|
||||
height: 48px;
|
||||
background: #2b2d31;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.inner {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@@ -40,7 +40,7 @@ export function Footer() {
|
||||
</a>
|
||||
</Text>
|
||||
<Divider orientation={'vertical'} />
|
||||
<Text size='sm'>© {new Date().getFullYear()} Aviation Data</Text>
|
||||
<Text size='sm'>© {new Date().getFullYear()} bensherriff.com</Text>
|
||||
</Group>
|
||||
{!isMobile && (
|
||||
<Group gap='xs' justify='flex-end' wrap='nowrap'>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
height: 56px;
|
||||
padding: 0 16px 0 16px;
|
||||
/*background-color: var(--mantine-color-body);*/
|
||||
background: #32495f;
|
||||
background: #2b2d31;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, Box, Burger, Button, Group, Text } from '@mantine/core';
|
||||
import { Avatar, Box, Burger, Button, Drawer, Group, Text } from '@mantine/core';
|
||||
import { useDisclosure, useMediaQuery, useToggle } from '@mantine/hooks';
|
||||
import classes from './Header.module.css';
|
||||
import { HeaderModal } from '@components/Header/HeaderModal.tsx';
|
||||
@@ -146,7 +146,7 @@ export function Header() {
|
||||
<Group justify='space-between' h='100%'>
|
||||
<Group align='center' gap='xs'>
|
||||
<Link to='/'>
|
||||
<Avatar src='/logo.svg' alt='logo' onClick={toggle} />
|
||||
<Avatar src='/logo.svg' alt='logo' />
|
||||
</Link>
|
||||
<Text size={'xl'}>Aviation Data</Text>
|
||||
</Group>
|
||||
@@ -173,6 +173,21 @@ export function Header() {
|
||||
</Group>
|
||||
</header>
|
||||
</Box>
|
||||
<Drawer.Root
|
||||
opened={opened}
|
||||
onClose={toggle}
|
||||
zIndex={1001}
|
||||
padding="md"
|
||||
size="40%"
|
||||
position="right"
|
||||
>
|
||||
<Drawer.Overlay />
|
||||
<Drawer.Content>
|
||||
<Drawer.Body>
|
||||
test
|
||||
</Drawer.Body>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
<HeaderModal type={modalType} toggle={modalToggle} login={loginUser} register={registerUser} />
|
||||
</>
|
||||
);
|
||||
|
||||
29
ui/src/components/MainLayout.tsx
Normal file
29
ui/src/components/MainLayout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AppShell } from '@mantine/core';
|
||||
import { Outlet } from 'react-router';
|
||||
import { Header } from '@components/Header';
|
||||
import { Footer } from '@components/Footer';
|
||||
|
||||
export function MainLayout() {
|
||||
return (
|
||||
<AppShell padding={0} mih={'100dvh'}>
|
||||
<AppShell.Header>
|
||||
<Header />
|
||||
</AppShell.Header>
|
||||
<AppShell.Main p={0} style={{ display: 'flex', minHeight: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</AppShell.Main>
|
||||
<AppShell.Footer>
|
||||
<Footer />
|
||||
</AppShell.Footer>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Header } from '@components/Header';
|
||||
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||
import { NotFound } from '@components/NotFound';
|
||||
|
||||
@@ -11,7 +10,6 @@ export function Profile() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
Todo: profile {user?.firstName}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
@@ -9,6 +10,7 @@ import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
import { Profile } from '@components/Profile.tsx';
|
||||
import { Administration } from '@components/Administration.tsx';
|
||||
import { NotFound } from '@components/NotFound';
|
||||
import { MainLayout } from '@components/MainLayout.tsx';
|
||||
|
||||
const theme = createTheme({
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
@@ -21,9 +23,11 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Notifications zIndex={2000} />
|
||||
<UserProvider>
|
||||
<Routes>
|
||||
<Route path='/' element={<App />} />
|
||||
<Route path='/profile' element={<Profile />} />
|
||||
<Route path='/administration' element={<Administration />} />
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<App />} />
|
||||
<Route path='profile' element={<Profile />} />
|
||||
<Route path='administration' element={<Administration />} />
|
||||
</Route>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
</UserProvider>
|
||||
|
||||
Reference in New Issue
Block a user