This commit is contained in:
2025-09-19 19:33:53 -04:00
parent 8844ee75fe
commit 84312d0b50
36 changed files with 799 additions and 694 deletions

View File

@@ -124,7 +124,7 @@ push: image=${registry}/aviation-${folder}:${version}
push: ## Build and push a specific docker image from a folder push: ## Build and push a specific docker image from a folder
docker buildx create \ docker buildx create \
--use \ --use \
--name aviation-builder \ --name default-builder \
--platform ${platform} || true; \ --platform ${platform} || true; \
docker buildx build \ docker buildx build \
-f ${folder}/Dockerfile \ -f ${folder}/Dockerfile \

91
Taskfile.yml Normal file
View 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
View File

@@ -64,7 +64,7 @@ dependencies = [
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rand 0.9.1", "rand 0.9.2",
"sha1", "sha1",
"smallvec", "smallvec",
"tokio", "tokio",
@@ -158,7 +158,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"mio", "mio",
"socket2", "socket2 0.5.9",
"tokio", "tokio",
"tracing", "tracing",
] ]
@@ -220,7 +220,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"socket2", "socket2 0.5.9",
"time", "time",
"tracing", "tracing",
"url", "url",
@@ -295,12 +295,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -362,7 +356,7 @@ dependencies = [
[[package]] [[package]]
name = "api" name = "api"
version = "0.1.2" version = "0.1.3"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-multipart", "actix-multipart",
@@ -378,7 +372,7 @@ dependencies = [
"handlebars", "handlebars",
"lettre", "lettre",
"log", "log",
"rand 0.9.1", "rand 0.9.2",
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"redis", "redis",
"regex", "regex",
@@ -496,9 +490,9 @@ dependencies = [
[[package]] [[package]]
name = "backon" name = "backon"
version = "1.5.0" version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d"
dependencies = [ dependencies = [
"fastrand", "fastrand",
] ]
@@ -624,17 +618,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link 0.2.0",
] ]
[[package]] [[package]]
@@ -1092,9 +1085,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys", "libz-rs-sys",
@@ -1297,9 +1290,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "governor" name = "governor"
version = "0.10.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbe789d04bf14543f03c4b60cd494148aa79438c8440ae7d81a7778147745c3" checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"dashmap", "dashmap",
@@ -1312,7 +1305,7 @@ dependencies = [
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
"quanta", "quanta",
"rand 0.9.1", "rand 0.9.2",
"smallvec", "smallvec",
"spinning_top", "spinning_top",
"web-time", "web-time",
@@ -1449,7 +1442,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -1536,7 +1529,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.9",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -1615,18 +1608,23 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
dependencies = [ dependencies = [
"base64",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.6.0", "hyper 1.6.0",
"ipnet",
"libc", "libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.9",
"system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry",
] ]
[[package]] [[package]]
@@ -1783,12 +1781,33 @@ dependencies = [
"serde", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@@ -1862,9 +1881,9 @@ dependencies = [
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.11.16" version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2" checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@@ -1882,7 +1901,7 @@ dependencies = [
"nom", "nom",
"percent-encoding", "percent-encoding",
"quoted_printable", "quoted_printable",
"socket2", "socket2 0.6.0",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"url", "url",
@@ -1890,9 +1909,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]] [[package]]
name = "libm" name = "libm"
@@ -1912,9 +1931,9 @@ dependencies = [
[[package]] [[package]]
name = "libz-rs-sys" name = "libz-rs-sys"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
dependencies = [ dependencies = [
"zlib-rs", "zlib-rs",
] ]
@@ -1960,9 +1979,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]] [[package]]
name = "maybe-async" name = "maybe-async"
@@ -2494,9 +2513,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.3", "rand_core 0.9.3",
@@ -2551,9 +2570,9 @@ dependencies = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "0.31.0" version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"backon", "backon",
@@ -2571,7 +2590,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha1_smol", "sha1_smol",
"socket2", "socket2 0.6.0",
"tokio", "tokio",
"tokio-util", "tokio-util",
"url", "url",
@@ -2588,9 +2607,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2623,15 +2642,14 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.15" version = "0.12.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util",
"h2 0.4.10", "h2 0.4.10",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
@@ -2640,29 +2658,26 @@ dependencies = [
"hyper-rustls", "hyper-rustls",
"hyper-tls 0.6.0", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls", "native-tls",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls-pemfile", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tower", "tower",
"tower-http",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows-registry",
] ]
[[package]] [[package]]
@@ -2822,15 +2837,6 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.12.0" version = "1.12.0"
@@ -2964,9 +2970,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -3093,6 +3099,16 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -3123,9 +3139,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx" name = "sqlx"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [ dependencies = [
"sqlx-core", "sqlx-core",
"sqlx-macros", "sqlx-macros",
@@ -3136,9 +3152,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-core" name = "sqlx-core"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -3172,9 +3188,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-macros" name = "sqlx-macros"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3185,9 +3201,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-macros-core" name = "sqlx-macros-core"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
@@ -3204,16 +3220,15 @@ dependencies = [
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn", "syn",
"tempfile",
"tokio", "tokio",
"url", "url",
] ]
[[package]] [[package]]
name = "sqlx-mysql" name = "sqlx-mysql"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64",
@@ -3255,9 +3270,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-postgres" name = "sqlx-postgres"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64",
@@ -3294,9 +3309,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-sqlite" name = "sqlx-sqlite"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono", "chrono",
@@ -3538,20 +3553,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.45.0" version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"io-uring",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "slab",
"socket2 0.6.0",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -3624,6 +3641,24 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -3762,9 +3797,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "utoipa" name = "utoipa"
version = "5.3.1" version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@@ -3785,9 +3820,9 @@ dependencies = [
[[package]] [[package]]
name = "utoipa-gen" name = "utoipa-gen"
version = "5.3.1" version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3816,12 +3851,14 @@ dependencies = [
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.16.0" version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"js-sys",
"serde", "serde",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -3969,9 +4006,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.0" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -4025,7 +4062,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link", "windows-link 0.1.1",
"windows-result", "windows-result",
"windows-strings 0.4.2", "windows-strings 0.4.2",
] ]
@@ -4058,6 +4095,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.4.0" version = "0.4.0"
@@ -4075,7 +4118,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -4084,7 +4127,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -4093,7 +4136,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -4443,9 +4486,9 @@ dependencies = [
[[package]] [[package]]
name = "zlib-rs" name = "zlib-rs"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
[[package]] [[package]]
name = "zopfli" name = "zopfli"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "api" name = "api"
version = "0.1.2" version = "0.1.3"
edition = "2024" edition = "2024"
authors = ["Ben Sherriff <ben@bensherriff.com>"] authors = ["Ben Sherriff <ben@bensherriff.com>"]
repository = "https://gitea.bensherriff.com/bsherriff/aviation" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "4.10.2" actix-web = "4.11.0"
actix-cors = "0.7.1" actix-cors = "0.7.1"
actix-multipart = "0.7.2" actix-multipart = "0.7.2"
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
dotenv = "0.15.0" 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" env_logger = "0.11.8"
reqwest = "0.12.15" reqwest = "0.12.23"
serde = {version = "1.0.219", features = ["derive"]} serde = {version = "1.0.219", features = ["derive"]}
serde_json = "1.0.140" serde_json = "1.0.142"
tokio = { version = "1.45.0", features = ["macros", "rt", "time"] } tokio = { version = "1.47.1", features = ["macros", "rt", "time"] }
uuid = { version = "1.16.0", features = ["serde", "v4"] } uuid = { version = "1.18.0", features = ["serde", "v4"] }
log = "0.4.27" log = "0.4.27"
argon2 = "0.5.3" 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" regex = "1.11.1"
futures-util = "0.3.31" futures-util = "0.3.31"
rust-s3 = "0.35.1" rust-s3 = "0.35.1"
rand = "0.9.1" rand = "0.9.2"
rand_chacha = "0.9.0" rand_chacha = "0.9.0"
futures = "0.3.31" futures = "0.3.31"
utoipa = { version = "5.3.1", features = ["chrono", "uuid", "actix_extras"] } utoipa = { version = "5.4.0", features = ["chrono", "uuid", "actix_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] } utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] }
utoipa-actix-web = "0.1.2" utoipa-actix-web = "0.1.2"
webpki-roots = "1.0.0" 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" handlebars = "6.3.2"
governor = "0.10.0" governor = "0.10.1"
flate2 = "1.1.1" flate2 = "1.1.2"

View File

@@ -1,11 +1,12 @@
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
use super::{SESSION_COOKIE_NAME, Session}; use super::{SESSION_COOKIE_NAME, Session};
use crate::account::user::User; use crate::account::user::User;
use crate::error::Error; 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 serde::{Deserialize, Serialize};
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Auth { pub struct Auth {
@@ -19,23 +20,31 @@ impl FromRequest for Auth {
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 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 match req
.headers() .headers()
.get(http::header::AUTHORIZATION) .get(http::header::AUTHORIZATION)
.map(|h| h.to_str().unwrap().split_at(7).1.to_string()) .map(|h| h.to_str().unwrap().split_at(7).1.to_string())
{ {
Some(key_id) => { Some(key_id) => {
let state = Arc::clone(&state);
let fut = async move { let fut = async move {
// Check if the Session API key exists // 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, Ok(session) => session,
Err(err) => { Err(err) => {
log::error!("Invalid session auth attempt: {}", err); log::error!("Invalid session auth attempt: {}", err);
return Err(Error::new(401, "API Key does not exist".to_string()).into()); return Err(Error::new(401, "API Key does not exist".to_string()).into());
} }
}; };
match User::select(&api_key.username).await { match User::select(&state.pool, &api_key.username).await {
Some(user) => Ok(Auth { Some(user) => Ok(Auth {
session_id: None, session_id: None,
api_key: Some(key_id), api_key: Some(key_id),
@@ -78,9 +87,10 @@ impl FromRequest for Auth {
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify the session // Verify the session
let state = Arc::clone(&state); // state: Arc<State>
let fut = async move { let fut = async move {
match Session::verify(&session_id, &ip_address).await { match Session::verify(&state, &session_id, &ip_address).await {
Ok(session) => match User::select(&session.username).await { Ok(session) => match User::select(&state.pool, &session.username).await {
Some(user) => Ok(Auth { Some(user) => Ok(Auth {
session_id: Some(session_id), session_id: Some(session_id),
api_key: None, api_key: None,

View File

@@ -1,12 +1,12 @@
use crate::account::{csprng, hash}; use crate::account::{csprng, hash};
use crate::db::redis_async_connection;
use crate::error::{ApiResult, Error}; use crate::error::{ApiResult, Error};
use crate::smtp; use crate::smtp;
use chrono::{Datelike, Utc}; use chrono::{Datelike, Utc};
use redis::{AsyncCommands, RedisResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use std::{env, fs}; use std::{env, fs};
use redis::aio::ConnectionManager;
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct EmailToken { pub struct EmailToken {
@@ -24,37 +24,25 @@ impl EmailToken {
} }
} }
pub async fn store(&self, ttl_secs: i64) -> ApiResult<()> { pub async fn store(&self, state: &AppState, ttl_secs: i64) -> ApiResult<()> {
let mut conn = redis_async_connection().await?;
let key = self.token.clone(); let key = self.token.clone();
let value = serde_json::to_string(self)?; let value = serde_json::to_string(self)?;
let now = Utc::now(); let now = Utc::now();
let expires_at = now + chrono::Duration::seconds(ttl_secs); let expires_at = now + chrono::Duration::seconds(ttl_secs);
let ttl = expires_at.timestamp() - now.timestamp(); 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 { match result {
Ok(_) => Ok(()), Some(value) => Ok(serde_json::from_str(&value)?),
Err(err) => Err(err.into()), None => Err(Error::new(404, format!("Missing email token {}", token))),
} }
} }
pub async fn get(token: &str) -> ApiResult<Self> { pub async fn delete(state: &AppState, token: &str) -> ApiResult<()> {
let mut conn = redis_async_connection().await?; state.del(token).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()),
}
} }
} }
@@ -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 token = csprng(128);
let email_token = EmailToken::new(email.to_string(), token, &ip_address); 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 base_url = env::var("EXTERNAL_URL")?;
let link = format!("{base_url}/profile/confirm?token={}", email_token.token); 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, ip_address,
err err
); );
let _ = EmailToken::delete(&email_token.token); let _ = EmailToken::delete(state, &email_token.token);
return Err(err); return Err(err);
} }

View File

@@ -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::{LoginRequest, RegisterRequest, UpdateUser, User, UserResponse};
use crate::account::user_favorites::UserFavorite; use crate::account::user_favorites::UserFavorite;
use crate::account::{Auth, csprng}; use crate::account::{Auth, csprng};
use crate::state::AppState;
#[utoipa::path( #[utoipa::path(
tag = "account", tag = "account",
@@ -24,7 +25,7 @@ use crate::account::{Auth, csprng};
) )
)] )]
#[post("/register")] #[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 register_user = user.into_inner();
let username = register_user.username.clone(); let username = register_user.username.clone();
let email = register_user.email.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), Err(err) => return ResponseError::error_response(&err),
}; };
match insert_user.insert().await { match insert_user.insert(&state.pool).await {
Ok(user) => { Ok(user) => {
let user_response: UserResponse = user.into(); let user_response: UserResponse = user.into();
log::info!( log::info!(
@@ -46,8 +47,8 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
// Send confirmation email // Send confirmation email
if let Some(email) = email { if let Some(email) = email {
if !email.is_empty() { if !email.is_empty() {
tokio::spawn(async move { tokio::task::spawn_local(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 confirmation email: {}", err); log::error!("Failed to send confirmation email: {}", err);
}; };
}); });
@@ -91,15 +92,16 @@ struct ConfirmEmail {
)] )]
#[post("/register/confirm")] #[post("/register/confirm")]
async fn confirm_email_registration( async fn confirm_email_registration(
state: web::Data<AppState>,
request: web::Json<ConfirmEmail>, request: web::Json<ConfirmEmail>,
req: HttpRequest, req: HttpRequest,
) -> HttpResponse { ) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
let token = &request.token; let token = &request.token;
let email_token = match EmailToken::get(token).await { let email_token = match EmailToken::get(&state, token).await {
Ok(password_reset) => { 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); return ResponseError::error_response(&err);
}; };
password_reset 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) => { Some(user) => {
let update_user = UpdateUser { let update_user = UpdateUser {
email: None, email: None,
@@ -121,7 +123,7 @@ async fn confirm_email_registration(
avatar: None, avatar: None,
}; };
match update_user.update(&user.username).await { match update_user.update(&state.pool, &user.username).await {
Ok(user) => { Ok(user) => {
let response: UserResponse = user.into(); let response: UserResponse = user.into();
log::info!( log::info!(
@@ -157,13 +159,13 @@ async fn confirm_email_registration(
) )
)] )]
#[post("/register/email")] #[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 email = auth.user.email;
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
match email { match email {
Some(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, Some(query_user) => query_user,
None => return HttpResponse::Unauthorized().finish(), None => return HttpResponse::Unauthorized().finish(),
}; };
@@ -174,11 +176,10 @@ async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse
} }
// Send reverify confirmation email // Send reverify confirmation email
tokio::spawn(async move { if let Err(err) = send_confirm_email(&state, &email, &ip_address).await {
if let Err(err) = send_confirm_email(&email, &ip_address).await { log::error!("Failed to send reverify confirmation email: {}", err);
log::error!("Failed to send reverify confirmation email: {}", err); return HttpResponse::InternalServerError().finish();
}; };
});
HttpResponse::Ok().finish() HttpResponse::Ok().finish()
} }
None => HttpResponse::NotFound().finish(), None => HttpResponse::NotFound().finish(),
@@ -195,11 +196,11 @@ async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse
), ),
)] )]
#[post("/login")] #[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 username = &request.username;
let ip_address = req.peer_addr().unwrap().ip().to_string(); 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, Some(query_user) => query_user,
None => return HttpResponse::Unauthorized().finish(), 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_cookie = session.cookie();
let session_exp_cookie = session.expiration_cookie(); let session_exp_cookie = session.expiration_cookie();
// Save the session to the database // Save the session to the database
if let Err(err) = session.store().await { if let Err(err) = session.store(&state).await {
log::error!( log::error!(
"Login attempt failure [User: {}] [IP Address: {}]: {}", "Login attempt failure [User: {}] [IP Address: {}]: {}",
username, username,
@@ -253,14 +254,14 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
) )
)] )]
#[post("/logout")] #[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 username = auth.user.username;
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
// Delete the session from the store // Delete the session from the store
match req.cookie(SESSION_COOKIE_NAME) { match req.cookie(SESSION_COOKIE_NAME) {
Some(cookie) => { Some(cookie) => {
let session_id = cookie.value().to_string(); 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!( log::error!(
"Logout attempt failure [User: {}] [IP Address: {}]: {}", "Logout attempt failure [User: {}] [IP Address: {}]: {}",
username, username,
@@ -302,14 +303,14 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
) )
)] )]
#[get("/profile")] #[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(); let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify a session cookie exists // Verify a session cookie exists
match req.cookie(SESSION_COOKIE_NAME) { match req.cookie(SESSION_COOKIE_NAME) {
// Validate the session // Validate the session
Some(cookie) => { Some(cookie) => {
let session_id = cookie.value().to_string(); 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, Ok(session) => session,
Err(_) => { Err(_) => {
log::error!( log::error!(
@@ -324,7 +325,7 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
} }
}; };
let username = &session.username; 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, Some(query_user) => query_user,
None => { None => {
return HttpResponse::Unauthorized() return HttpResponse::Unauthorized()
@@ -366,14 +367,14 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
) )
)] )]
#[post("/session")] #[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(); let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify a session cookie exists // Verify a session cookie exists
match req.cookie(SESSION_COOKIE_NAME) { match req.cookie(SESSION_COOKIE_NAME) {
// Validate the session // Validate the session
Some(cookie) => { Some(cookie) => {
let session_id = cookie.value().to_string(); 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, Ok(session) => session,
Err(_) => { Err(_) => {
log::error!( log::error!(
@@ -428,6 +429,7 @@ struct ChangePassword {
)] )]
#[put("/password")] #[put("/password")]
async fn change_password( async fn change_password(
state: web::Data<AppState>,
request: web::Json<ChangePassword>, request: web::Json<ChangePassword>,
req: HttpRequest, req: HttpRequest,
auth: Auth, auth: Auth,
@@ -435,7 +437,7 @@ async fn change_password(
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
let username = auth.user.username; 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(); return HttpResponse::Unauthorized().finish();
}; };
@@ -449,7 +451,7 @@ async fn change_password(
avatar: None, avatar: None,
}; };
match update_user.update(&username).await { match update_user.update(&state.pool, &username).await {
Ok(user) => { Ok(user) => {
let response: UserResponse = user.into(); let response: UserResponse = user.into();
log::info!( log::info!(
@@ -486,18 +488,18 @@ struct PasswordReset {
) )
)] )]
#[post("/password/reset")] #[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 email = &request.email;
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
let token = csprng(128); let token = csprng(128);
// Silently return if the user's email does not exist // 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(); return HttpResponse::Ok().finish();
}; };
let email_token = EmailToken::new(email.clone(), token, &ip_address); 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); return ResponseError::error_response(&err);
} }
@@ -525,6 +527,7 @@ struct ConfirmPasswordReset {
)] )]
#[post("/password/reset/confirm")] #[post("/password/reset/confirm")]
async fn confirm_password_reset( async fn confirm_password_reset(
state: web::Data<AppState>,
request: web::Json<ConfirmPasswordReset>, request: web::Json<ConfirmPasswordReset>,
req: HttpRequest, req: HttpRequest,
) -> HttpResponse { ) -> HttpResponse {
@@ -532,9 +535,9 @@ async fn confirm_password_reset(
let ip_address = req.peer_addr().unwrap().ip().to_string(); let ip_address = req.peer_addr().unwrap().ip().to_string();
let token = &request.token; let token = &request.token;
let email_token = match EmailToken::get(token).await { let email_token = match EmailToken::get(&state, token).await {
Ok(password_reset) => { 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); return ResponseError::error_response(&err);
}; };
password_reset password_reset
@@ -558,9 +561,9 @@ async fn confirm_password_reset(
) )
)] )]
#[get("/profile/favorites")] #[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; 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), Ok(favorites) => HttpResponse::Ok().json(favorites),
Err(err) => ResponseError::error_response(&err), Err(err) => ResponseError::error_response(&err),
} }
@@ -577,9 +580,9 @@ async fn get_favorites(auth: Auth) -> HttpResponse {
) )
)] )]
#[post("/profile/favorites/{icao}")] #[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; 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(), Ok(_) => HttpResponse::Ok().finish(),
Err(err) => ResponseError::error_response(&err), 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}")] #[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; 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(), Ok(_) => HttpResponse::Ok().finish(),
Err(err) => ResponseError::error_response(&err), Err(err) => ResponseError::error_response(&err),
} }

View File

@@ -1,13 +1,12 @@
use super::{csprng, hash, verify_hash}; use super::{csprng, hash, verify_hash};
use crate::{ use crate::error::{ApiResult, Error};
db::redis_async_connection,
error::{ApiResult, Error},
};
use actix_web::cookie::{Cookie, time::Duration}; use actix_web::cookie::{Cookie, time::Duration};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use redis::{AsyncCommands, RedisResult}; use redis::{AsyncCommands, RedisResult};
use redis::aio::ConnectionManager;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::task; use tokio::task;
use crate::state::AppState;
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
pub const SESSION_COOKIE_NAME: &str = "session"; pub const SESSION_COOKIE_NAME: &str = "session";
@@ -40,16 +39,15 @@ impl Session {
} }
} }
pub async fn store(&self) -> ApiResult<()> { pub async fn store(&self, state: &AppState) -> ApiResult<()> {
let mut conn = redis_async_connection().await?;
let key = self.session_id.clone(); let key = self.session_id.clone();
let value = serde_json::to_string(self)?; let value = serde_json::to_string(self)?;
let result: RedisResult<()> = match self.expires_at { let result: ApiResult<()> = match self.expires_at {
Some(expires_at) => { Some(expires_at) => {
let ttl = expires_at.timestamp() - Utc::now().timestamp(); 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 { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -57,43 +55,33 @@ impl Session {
} }
} }
pub async fn get(session_id: &str) -> ApiResult<Self> { pub async fn get(state: &AppState, session_id: &str) -> ApiResult<Self> {
let mut conn = redis_async_connection().await?; let result: Option<String> = state.get(session_id).await?;
let result: RedisResult<Option<String>> = conn.get(session_id).await;
match result { match result {
Ok(Some(value)) => Ok(serde_json::from_str(&value)?), Some(value) => Ok(serde_json::from_str(&value)?),
Ok(None) => Err(Error::new(401, format!("Missing session {}", session_id))), None => Err(Error::new(401, format!("Missing session {}", session_id))),
Err(err) => Err(err.into()),
} }
} }
pub async fn replace(session_id: &str, ip_address: &str) -> ApiResult<Self> { pub async fn replace(state: &AppState, session_id: &str, ip_address: &str) -> ApiResult<Self> {
let mut session = Self::verify(session_id, ip_address).await?; let mut session = Self::verify(state, session_id, ip_address).await?;
let session_id_owned = session_id.to_owned(); let session_id_owned = session_id.to_owned();
task::spawn(async move { Self::delete(state, &session_id_owned).await?;
if let Err(err) = Self::delete(&session_id_owned).await {
log::error!(
"Error deleting old session in replace session call: {}",
err
);
};
});
session = Session::default(&session.username, ip_address); session = Session::default(&session.username, ip_address);
session.store().await?; session.store(state).await?;
Ok(session) Ok(session)
} }
pub async fn delete(session_id: &str) -> ApiResult<()> { pub async fn delete(state: &AppState, session_id: &str) -> ApiResult<()> {
let mut conn = redis_async_connection().await?; let result: ApiResult<()> = state.del(session_id).await;
let result: RedisResult<()> = conn.del(session_id).await;
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
} }
} }
pub async fn verify(session_id: &str, ip_address: &str) -> ApiResult<Self> { pub async fn verify(state: &AppState, session_id: &str, ip_address: &str) -> ApiResult<Self> {
let session = Self::get(session_id).await?; let session = Self::get(state, session_id).await?;
// Check if the IP Address matches the Session's IP Address // Check if the IP Address matches the Session's IP Address
if verify_hash(ip_address, &session.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 { let expires_at = match self.expires_at {
Some(expires_at) => expires_at.timestamp(), Some(expires_at) => expires_at.timestamp(),
None => DEFAULT_SESSION_TTL, None => DEFAULT_SESSION_TTL,
@@ -131,7 +119,7 @@ impl Session {
cookie cookie
} }
pub fn expiration_cookie(&self) -> Cookie { pub fn expiration_cookie(&self) -> Cookie<'_> {
let expires_at = match self.expires_at { let expires_at = match self.expires_at {
Some(expires_at) => expires_at.timestamp(), Some(expires_at) => expires_at.timestamp(),
None => DEFAULT_SESSION_TTL, None => DEFAULT_SESSION_TTL,

View File

@@ -1,10 +1,9 @@
use crate::db;
use crate::{account::hash, error::ApiResult}; use crate::{account::hash, error::ApiResult};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[allow(unused_imports)] // Import is used in schema examples #[allow(unused_imports)] // Import is used in schema examples
use serde_json::json; use serde_json::json;
use sqlx::{Postgres, QueryBuilder}; use sqlx::{Pool, Postgres, QueryBuilder};
use utoipa::ToSchema; use utoipa::ToSchema;
pub const ADMIN_ROLE: &str = "ADMIN"; pub const ADMIN_ROLE: &str = "ADMIN";
@@ -107,9 +106,7 @@ pub struct UpdateUser {
} }
impl UpdateUser { impl UpdateUser {
pub async fn update(&self, username: &str) -> ApiResult<User> { pub async fn update(&self, pool: &Pool<Postgres>, username: &str) -> ApiResult<User> {
let pool = db::pool();
let mut query_builder: QueryBuilder<Postgres> = let mut query_builder: QueryBuilder<Postgres> =
QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME)); QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME));
@@ -189,8 +186,7 @@ pub struct User {
} }
impl User { impl User {
pub async fn select(username: &str) -> Option<Self> { pub async fn select(pool: &Pool<Postgres>, username: &str) -> Option<Self> {
let pool = db::pool();
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!( let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
r#" r#"
SELECT * FROM {} WHERE username = $1 SELECT * FROM {} WHERE username = $1
@@ -208,8 +204,7 @@ impl User {
user user
} }
pub async fn select_by_email(email: &str) -> Option<Self> { pub async fn select_by_email(pool: &Pool<Postgres>, email: &str) -> Option<Self> {
let pool = db::pool();
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!( let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
r#" r#"
SELECT * FROM {} WHERE email = $1 SELECT * FROM {} WHERE email = $1
@@ -228,9 +223,7 @@ impl User {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub async fn count() -> i64 { pub async fn count(pool: &Pool<Postgres>) -> i64 {
let pool = db::pool();
sqlx::query_scalar(&format!( sqlx::query_scalar(&format!(
r#" r#"
SELECT COUNT(*) FROM {} SELECT COUNT(*) FROM {}
@@ -242,8 +235,7 @@ impl User {
.unwrap_or_else(|_| 0) .unwrap_or_else(|_| 0)
} }
pub async fn insert(&self) -> ApiResult<User> { pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<User> {
let pool = db::pool();
let user: User = sqlx::query_as::<_, Self>(&format!( let user: User = sqlx::query_as::<_, Self>(&format!(
r#" r#"
INSERT INTO {} ( INSERT INTO {} (

View File

@@ -1,6 +1,6 @@
use crate::db;
use crate::error::ApiResult; use crate::error::ApiResult;
use serde::Deserialize; use serde::Deserialize;
use sqlx::{Pool, Postgres};
const TABLE_NAME: &str = "user_airport_favorites"; const TABLE_NAME: &str = "user_airport_favorites";
@@ -11,8 +11,7 @@ pub struct UserFavorite {
} }
impl UserFavorite { impl UserFavorite {
pub async fn select_all(username: &str) -> ApiResult<Vec<String>> { pub async fn select_all(pool: &Pool<Postgres>, username: &str) -> ApiResult<Vec<String>> {
let pool = db::pool();
let user_favorites: Vec<UserFavorite> = sqlx::query_as::<_, UserFavorite>(&format!( let user_favorites: Vec<UserFavorite> = sqlx::query_as::<_, UserFavorite>(&format!(
r#" r#"
SELECT * FROM {} WHERE username = $1 SELECT * FROM {} WHERE username = $1
@@ -28,8 +27,7 @@ impl UserFavorite {
Ok(favorites) Ok(favorites)
} }
pub async fn insert(username: &str, icao: &str) -> ApiResult<()> { pub async fn insert(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!( sqlx::query(&format!(
r#" r#"
INSERT INTO {} ( INSERT INTO {} (
@@ -46,8 +44,7 @@ impl UserFavorite {
Ok(()) Ok(())
} }
pub async fn delete(username: &str, icao: &str) -> ApiResult<()> { pub async fn delete(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!( sqlx::query(&format!(
r#" r#"
DELETE FROM {} WHERE username = $1 AND icao = $2 DELETE FROM {} WHERE username = $1 AND icao = $2

View File

@@ -2,16 +2,16 @@ use crate::airports::{
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication, AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
UpdateRunway, UpdateRunway,
}; };
use crate::db;
use crate::error::{ApiResult, Error}; use crate::error::{ApiResult, Error};
use crate::metars::Metar; use crate::metars::Metar;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures_util::try_join; use futures_util::try_join;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Execute, Postgres, QueryBuilder}; use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
use crate::state::AppState;
const TABLE_NAME: &str = "airports"; const TABLE_NAME: &str = "airports";
const DEFAULT_COLUMNS: &str = "icao, iata, local, name, category, iso_country, \ const DEFAULT_COLUMNS: &str = "icao, iata, local, name, category, iso_country, \
@@ -255,8 +255,7 @@ impl From<AirportRow> for Airport {
} }
impl Airport { impl Airport {
pub async fn select(icao: &str, metar: bool) -> Option<Self> { pub async fn select(pool: &Pool<Postgres>, icao: &str, metar: bool) -> Option<Self> {
let pool = db::pool();
let airport_fut = async { let airport_fut = async {
sqlx::query_as(&format!( sqlx::query_as(&format!(
@@ -270,7 +269,7 @@ impl Airport {
let metar_fut = async { let metar_fut = async {
if metar { 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)), Ok(m) => Some(m.into_iter().nth(0)),
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);
@@ -282,8 +281,8 @@ impl Airport {
} }
}; };
let runways_fut = Runway::select_all(icao); let runways_fut = Runway::select_all(pool, icao);
let communications_fut = Communication::select_all(icao); let communications_fut = Communication::select_all(pool, icao);
let (airport_result, runways_result, communications_result, metar_result) = let (airport_result, runways_result, communications_result, metar_result) =
tokio::join!(airport_fut, runways_fut, communications_fut, metar_fut); 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>> { pub async fn select_all(pool: &Pool<Postgres>, query: &AirportQuery) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let mut builder = let mut builder =
QueryBuilder::<Postgres>::new(format!("SELECT {} FROM {}", DEFAULT_COLUMNS, TABLE_NAME)); QueryBuilder::<Postgres>::new(format!("SELECT {} FROM {}", DEFAULT_COLUMNS, TABLE_NAME));
@@ -410,10 +407,10 @@ impl Airport {
// Bulk update airport subfields // Bulk update airport subfields
let icaos: Vec<String> = airports.iter().map(|a| a.icao.to_uppercase()).collect(); let icaos: Vec<String> = airports.iter().map(|a| a.icao.to_uppercase()).collect();
let runway_future = Runway::select_all_map(&icaos); let runway_future = Runway::select_all_map(pool, &icaos);
let frequency_future = Communication::select_all_map(&icaos); let frequency_future = Communication::select_all_map(pool, &icaos);
let metar_future = if query.metars.unwrap_or(false) { let metar_future = if query.metars.unwrap_or(false) {
Some(Metar::get_all_distinct(&icaos)) Some(Metar::get_all_distinct(pool, &icaos))
} else { } else {
None None
}; };
@@ -453,9 +450,7 @@ impl Airport {
Ok(airports) Ok(airports)
} }
pub async fn count(query: &AirportQuery) -> i64 { pub async fn count(pool: &Pool<Postgres>, query: &AirportQuery) -> i64 {
let pool = db::pool();
let mut builder = QueryBuilder::<Postgres>::new("SELECT COUNT(*) FROM "); let mut builder = QueryBuilder::<Postgres>::new("SELECT COUNT(*) FROM ");
builder.push(TABLE_NAME); builder.push(TABLE_NAME);
@@ -492,9 +487,7 @@ impl Airport {
sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0) sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0)
} }
pub async fn insert(&self) -> ApiResult<Self> { pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<Self> {
let pool = db::pool();
let mut all_runway_rows: Vec<RunwayRow> = Vec::new(); let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new(); let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new();
for runway in &self.runways { for runway in &self.runways {
@@ -503,8 +496,8 @@ impl Airport {
for frequency in &self.communications { for frequency in &self.communications {
all_frequency_rows.push(Communication::into(frequency, &self.icao)); all_frequency_rows.push(Communication::into(frequency, &self.icao));
} }
Runway::insert_all(&all_runway_rows).await?; Runway::insert_all(pool, &all_runway_rows).await?;
Communication::insert_all(&all_frequency_rows).await?; Communication::insert_all(pool, &all_frequency_rows).await?;
let airport: AirportRow = sqlx::query_as(&format!( let airport: AirportRow = sqlx::query_as(&format!(
r#" r#"
@@ -542,8 +535,7 @@ impl Airport {
Ok(airport.into()) Ok(airport.into())
} }
pub async fn insert_all(airports: Vec<Self>) -> ApiResult<()> { pub async fn insert_all(pool: &Pool<Postgres>, airports: Vec<Self>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000; let chunk_size = 1000;
let mut all_runway_rows: Vec<RunwayRow> = Vec::new(); let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new(); let mut all_frequency_rows: Vec<CommunicationRow> = Vec::new();
@@ -593,16 +585,14 @@ impl Airport {
query.execute(pool).await?; query.execute(pool).await?;
} }
Runway::insert_all(&all_runway_rows).await?; Runway::insert_all(pool, &all_runway_rows).await?;
Communication::insert_all(&all_frequency_rows).await?; Communication::insert_all(pool, &all_frequency_rows).await?;
Ok(()) Ok(())
} }
// TODO // TODO
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> { pub async fn update(pool: &Pool<Postgres>, icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
let pool = db::pool();
let mut query_builder: QueryBuilder<Postgres> = let mut query_builder: QueryBuilder<Postgres> =
QueryBuilder::new(format!("UPDATE {} SET ", TABLE_NAME)); QueryBuilder::new(format!("UPDATE {} SET ", TABLE_NAME));
if let Some(latest_metar_observation) = airport.latest_metar_observation { if let Some(latest_metar_observation) = airport.latest_metar_observation {
@@ -617,9 +607,7 @@ impl Airport {
Ok(()) Ok(())
} }
pub async fn delete(icao: &str) -> ApiResult<()> { pub async fn delete(pool: &Pool<Postgres>, icao: &str) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!( sqlx::query(&format!(
r#" r#"
DELETE FROM {} WHERE icao = $1 DELETE FROM {} WHERE icao = $1
@@ -633,9 +621,7 @@ impl Airport {
Ok(()) Ok(())
} }
pub async fn delete_all() -> ApiResult<()> { pub async fn delete_all(pool: &Pool<Postgres>) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!( sqlx::query(&format!(
r#" r#"
DELETE FROM {} WHERE true DELETE FROM {} WHERE true

View File

@@ -1,7 +1,6 @@
use crate::db;
use crate::error::ApiResult; use crate::error::ApiResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder}; use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashMap; use std::collections::HashMap;
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
@@ -65,9 +64,7 @@ impl Communication {
} }
} }
pub async fn select_all_map(icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> { pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
let pool = db::pool();
let frequency_rows: Vec<CommunicationRow> = sqlx::query_as(&format!( let frequency_rows: Vec<CommunicationRow> = sqlx::query_as(&format!(
r#"SELECT * FROM {} WHERE icao = ANY($1)"#, r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
TABLE_NAME TABLE_NAME
@@ -89,9 +86,7 @@ impl Communication {
Ok(frequency_map) Ok(frequency_map)
} }
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> { pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let frequency_row: Vec<CommunicationRow> = sqlx::query_as(&format!( let frequency_row: Vec<CommunicationRow> = sqlx::query_as(&format!(
r#" r#"
SELECT * FROM {} WHERE icao = $1 SELECT * FROM {} WHERE icao = $1
@@ -104,8 +99,7 @@ impl Communication {
Ok(frequency_row.into_iter().map(From::from).collect()) Ok(frequency_row.into_iter().map(From::from).collect())
} }
pub async fn insert_all(communications: &Vec<CommunicationRow>) -> ApiResult<()> { pub async fn insert_all(pool: &Pool<Postgres>, communications: &Vec<CommunicationRow>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000; let chunk_size = 1000;
for chunk in communications.chunks(chunk_size) { for chunk in communications.chunks(chunk_size) {

View File

@@ -1,7 +1,6 @@
use crate::db;
use crate::error::ApiResult; use crate::error::ApiResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder}; use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashMap; use std::collections::HashMap;
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
@@ -64,9 +63,7 @@ impl Runway {
} }
} }
pub async fn select_all_map(icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> { pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
let pool = db::pool();
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!( let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
r#"SELECT * FROM {} WHERE icao = ANY($1)"#, r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
TABLE_NAME TABLE_NAME
@@ -85,9 +82,7 @@ impl Runway {
Ok(runway_map) Ok(runway_map)
} }
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> { pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!( let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
r#" r#"
SELECT * FROM {} WHERE icao = $1 SELECT * FROM {} WHERE icao = $1
@@ -100,8 +95,7 @@ impl Runway {
Ok(runway_rows.into_iter().map(From::from).collect()) Ok(runway_rows.into_iter().map(From::from).collect())
} }
pub async fn insert_all(runways: &Vec<RunwayRow>) -> ApiResult<()> { pub async fn insert_all(pool: &Pool<Postgres>, runways: &Vec<RunwayRow>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000; let chunk_size = 1000;
for chunk in runways.chunks(chunk_size) { for chunk in runways.chunks(chunk_size) {

View File

@@ -5,13 +5,14 @@ use crate::airports::{AirportQuery, UpdateAirport};
use crate::{ use crate::{
account::{Auth, verify_role}, account::{Auth, verify_role},
airports::Airport, airports::Airport,
db::Paged,
}; };
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web}; use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web};
use utoipa::ToSchema; use utoipa::ToSchema;
use utoipa_actix_web::scope; use utoipa_actix_web::scope;
use utoipa_actix_web::service_config::ServiceConfig; use utoipa_actix_web::service_config::ServiceConfig;
use crate::state::AppState;
use crate::utils::Paged;
#[derive(ToSchema)] #[derive(ToSchema)]
#[allow(unused)] #[allow(unused)]
@@ -34,7 +35,7 @@ struct FileUpload {
) )
)] )]
#[post("/import")] #[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) { if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
return ResponseError::error_response(&err); 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(_) => {} Ok(_) => {}
Err(err) => return ResponseError::error_response(&err), Err(err) => return ResponseError::error_response(&err),
}; };
@@ -85,7 +86,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
), ),
)] )]
#[get("")] #[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()) { let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
Ok(q) => q.into_inner(), Ok(q) => q.into_inner(),
Err(err) => { 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 page = query.page.unwrap_or(1);
let mut limit = query.limit.unwrap_or(total as u32); let mut limit = query.limit.unwrap_or(total as u32);
if limit > 1000 { if limit > 1000 {
@@ -103,7 +104,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
query.limit = Some(limit); query.limit = Some(limit);
query.page = Some(page); query.page = Some(page);
match Airport::select_all(&query).await { match Airport::select_all(&state.pool, &query).await {
Ok(airports) => HttpResponse::Ok().json(Paged { Ok(airports) => HttpResponse::Ok().json(Paged {
data: airports, data: airports,
page, page,
@@ -125,7 +126,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
), ),
)] )]
#[get("/{icao}")] #[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()) { let metar = match web::Query::<AirportQuery>::from_query(req.query_string()) {
Ok(q) => q.metars.unwrap_or_else(|| false), Ok(q) => q.metars.unwrap_or_else(|| false),
Err(err) => { 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), Some(airport) => HttpResponse::Ok().json(airport),
None => HttpResponse::NotFound().finish(), None => HttpResponse::NotFound().finish(),
} }
@@ -152,12 +153,12 @@ async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse
) )
)] )]
#[post("")] #[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) { let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {} Ok(_) => {}
Err(err) => return ResponseError::error_response(&err), Err(err) => return ResponseError::error_response(&err),
}; };
match airport.insert().await { match airport.insert(&state.pool).await {
Ok(a) => HttpResponse::Ok().json(a), Ok(a) => HttpResponse::Ok().json(a),
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);
@@ -178,6 +179,7 @@ async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse
)] )]
#[put("/{icao}")] #[put("/{icao}")]
async fn update_airport( async fn update_airport(
state: web::Data<AppState>,
icao: web::Path<String>, icao: web::Path<String>,
airport: web::Json<UpdateAirport>, airport: web::Json<UpdateAirport>,
auth: Auth, auth: Auth,
@@ -186,7 +188,7 @@ async fn update_airport(
Ok(_) => {} Ok(_) => {}
Err(err) => return ResponseError::error_response(&err), 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), Ok(a) => HttpResponse::Ok().json(a),
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);
@@ -206,12 +208,12 @@ async fn update_airport(
) )
)] )]
#[delete("")] #[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) { let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {} Ok(_) => {}
Err(err) => return ResponseError::error_response(&err), Err(err) => return ResponseError::error_response(&err),
}; };
match Airport::delete_all().await { match Airport::delete_all(&state.pool).await {
Ok(_) => HttpResponse::NoContent().finish(), Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);
@@ -231,12 +233,12 @@ async fn delete_airports(auth: Auth) -> HttpResponse {
) )
)] )]
#[delete("/{icao}")] #[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) { let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {} Ok(_) => {}
Err(err) => return ResponseError::error_response(&err), 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(), Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);

View File

@@ -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 {}", &region.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,
}

View File

@@ -5,6 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::fmt; use std::fmt;
use std::sync::{MutexGuard, PoisonError};
pub type ApiResult<T> = Result<T, Error>; pub type ApiResult<T> = Result<T, Error>;
@@ -15,10 +16,10 @@ pub struct Error {
} }
impl Error { impl Error {
pub fn new(status: u16, message: String) -> Self { pub fn new(status: u16, details: String) -> Self {
Self { Self {
status, status,
details: message, details,
} }
} }
@@ -236,3 +237,9 @@ impl From<regex::Error> for Error {
Self::new(500, error.to_string()) 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())
}
}

View File

@@ -1,6 +1,5 @@
use crate::account::User; use crate::account::User;
use crate::account::{ADMIN_ROLE, hash}; use crate::account::{ADMIN_ROLE, hash};
use crate::http_client::HttpClient;
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{App, HttpServer, middleware::Logger, web}; use actix_web::{App, HttpServer, middleware::Logger, web};
use dotenv::from_filename; use dotenv::from_filename;
@@ -10,27 +9,24 @@ use utoipa::openapi::SecurityRequirement;
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
use utoipa_actix_web::{AppExt, scope}; use utoipa_actix_web::{AppExt, scope};
use utoipa_swagger_ui::{Config, SwaggerUi}; use utoipa_swagger_ui::{Config, SwaggerUi};
use crate::state::AppState;
mod account; mod account;
mod airports; mod airports;
mod db;
mod error; mod error;
mod http_client; mod http_client;
mod metars; mod metars;
mod scheduler; mod scheduler;
mod smtp; mod smtp;
mod system; mod system;
mod state;
#[derive(Debug, Clone)] mod utils;
struct AppState {
client: Arc<HttpClient>,
}
#[actix_web::main] #[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
initialize_environment()?; initialize_environment()?;
db::initialize().await?; let state = Arc::new(AppState::new().await?);
scheduler::run(); scheduler::run(state.clone());
// Initialize admin user // Initialize admin user
let admin_username = env::var("ADMIN_USERNAME"); 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() { if admin_username.is_ok() && admin_email.is_ok() && admin_password.is_ok() {
let username = admin_username.unwrap(); let username = admin_username.unwrap();
let email = admin_email.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"); log::debug!("Creating default administrator");
let password = admin_password.unwrap(); let password = admin_password.unwrap();
let password_hash = hash(&password)?; let password_hash = hash(&password)?;
@@ -60,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
updated_at: Default::default(), updated_at: Default::default(),
created_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"), Ok(_) => log::debug!("Default administrator was successfully created"),
Err(err) => { Err(err) => {
log::warn!("{}", 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 host = "0.0.0.0";
let port = env::var("API_PORT").unwrap_or("5000".to_string()); let port = env::var("API_PORT").unwrap_or("5000".to_string());

View File

@@ -1,9 +1,8 @@
use crate::db::redis_async_connection;
use crate::error::ApiResult; use crate::error::ApiResult;
use crate::metars::Metar; use crate::metars::Metar;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use redis::{AsyncCommands, RedisResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct MetarCheck { pub struct MetarCheck {
@@ -14,8 +13,8 @@ pub struct MetarCheck {
} }
impl MetarCheck { impl MetarCheck {
pub async fn new(icao: String, status: bool) -> Self { pub async fn new(state: &AppState, icao: String, status: bool) -> Self {
match Self::get(&icao).await { match Self::get(state, &icao).await {
Some(c) => Self { Some(c) => Self {
icao, icao,
status, status,
@@ -31,15 +30,8 @@ impl MetarCheck {
} }
} }
pub async fn get(icao: &str) -> Option<MetarCheck> { pub async fn get(state: &AppState, icao: &str) -> Option<MetarCheck> {
let mut conn = match redis_async_connection().await { let result: ApiResult<Option<String>> = state.get(icao).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;
match result { match result {
Ok(Some(value)) => match serde_json::from_str(&value) { Ok(Some(value)) => match serde_json::from_str(&value) {
Ok(result) => Some(result), Ok(result) => Some(result),
@@ -56,10 +48,9 @@ impl MetarCheck {
} }
} }
pub async fn insert(&self) -> ApiResult<()> { pub async fn insert(&self, state: &AppState) -> ApiResult<()> {
let mut conn = redis_async_connection().await?;
let value = serde_json::to_string(&self)?; let value = serde_json::to_string(&self)?;
conn.set::<_, _, ()>(self.icao.as_str(), value).await?; state.set(self.icao.as_str(), &value).await?;
Ok(()) Ok(())
} }

View File

@@ -1,21 +1,22 @@
use crate::airports::{Airport, UpdateAirport}; use crate::airports::{Airport, UpdateAirport};
use crate::error::Error; use crate::error::Error;
use crate::http_client::HttpClient;
use crate::metars::MetarCheck; use crate::metars::MetarCheck;
use crate::metars::utils::parse_metar_time; use crate::metars::utils::parse_metar_time;
use crate::{db, error::ApiResult}; use crate::error::ApiResult;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use reqwest::header::ETAG; use reqwest::header::ETAG;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder}; use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
use std::fmt::Display; use std::fmt::Display;
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use std::str::FromStr; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use regex::Regex;
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::state::AppState;
static TIME_OFFSET: OnceLock<i64> = OnceLock::new(); static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
@@ -278,8 +279,7 @@ struct MetarRow {
} }
impl MetarRow { impl MetarRow {
async fn insert(&self) -> ApiResult<()> { async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!( sqlx::query(&format!(
r#" r#"
INSERT INTO {} ( INSERT INTO {} (
@@ -305,8 +305,7 @@ impl MetarRow {
Ok(()) Ok(())
} }
async fn insert_all(metars: Vec<Metar>) -> ApiResult<()> { async fn insert_all(pool: &Pool<Postgres>, metars: Vec<Metar>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000; let chunk_size = 1000;
for chunk in metars.chunks(chunk_size) { for chunk in metars.chunks(chunk_size) {
@@ -342,10 +341,10 @@ impl MetarRow {
} }
impl Metar { 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![]; let mut metars: Vec<Self> = vec![];
for metar_string in metar_strings { for metar_string in metar_strings {
match Self::parse(metar_string) { match Self::parse(pool, metar_string) {
Ok(metar) => metars.push(metar), Ok(metar) => metars.push(metar),
Err(e) => { Err(e) => {
log::warn!("Failed to parse metar string: {}", e); log::warn!("Failed to parse metar string: {}", e);
@@ -357,7 +356,7 @@ impl Metar {
Ok(metars) Ok(metars)
} }
fn parse(metar_string: &str) -> ApiResult<Self> { fn parse(pool: &Pool<Postgres>, metar_string: &str) -> ApiResult<Self> {
if metar_string.is_empty() { if metar_string.is_empty() {
return Err(Error::new( return Err(Error::new(
404, 404,
@@ -368,7 +367,11 @@ impl Metar {
log::trace!("Parsing METAR data: {}", metar_string); log::trace!("Parsing METAR data: {}", metar_string);
let mut metar: Self = Self::default(); let mut metar: Self = Self::default();
metar.raw_text = metar_string.to_owned(); 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 { if metar_parts.len() < 4 {
return Err(Error::new( return Err(Error::new(
500, 500,
@@ -380,8 +383,14 @@ impl Metar {
} }
// Remove METAR at the start of the text // 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); 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 // Station Identifier
@@ -880,8 +889,10 @@ impl Metar {
// Update the airport's metar observation time // Update the airport's metar observation time
let icao = metar.icao.clone(); let icao = metar.icao.clone();
let observation_time = metar.observation_time.clone(); let observation_time = metar.observation_time.clone();
let pool = pool.clone();
tokio::spawn(async move { tokio::spawn(async move {
match Airport::update( match Airport::update(
&pool.clone(),
&icao, &icao,
&UpdateAirport { &UpdateAirport {
icao: None, icao: None,
@@ -982,13 +993,13 @@ impl Metar {
} }
pub async fn get_cached_remote_metars( pub async fn get_cached_remote_metars(
client: &HttpClient, state: &AppState,
etag: Option<String>, etag: Option<String>,
) -> ApiResult<(Vec<Self>, String)> { ) -> ApiResult<(Vec<Self>, String)> {
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set"); 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); 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) => { Ok(r) => {
let new_etag = r let new_etag = r
.headers() .headers()
@@ -1006,7 +1017,7 @@ impl Metar {
for line in text.lines() { for line in text.lines() {
// Split off the first column // Split off the first column
let raw_text = line.splitn(2, ',').next().unwrap(); 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), Ok(m) => output.push(m),
Err(err) => { Err(err) => {
log::warn!("{}", 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"); 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 // Query the remote API for the missing METAR data 10 at a time
let icao_chunks = icaos let icao_chunks = icaos
@@ -1039,7 +1050,7 @@ impl Metar {
"{}/api/data/metar?ids={}&hours=0&order=id,-obs", "{}/api/data/metar?ids={}&hours=0&order=id,-obs",
base_url, icao_chunk 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) => match r.text().await {
Ok(r) => { Ok(r) => {
let metar_chunk = r let metar_chunk = r
@@ -1047,7 +1058,7 @@ impl Metar {
.split("\n") .split("\n")
.filter(|m| !m.trim().is_empty()) .filter(|m| !m.trim().is_empty())
.collect(); .collect();
match Self::parse_multiple(&metar_chunk) { match Self::parse_multiple(&state.pool, &metar_chunk) {
Ok(m) => m, Ok(m) => m,
Err(err) => return Err(err), 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() { if icao_list.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let pool = db::pool();
let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!( let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!(
r#" r#"
SELECT DISTINCT ON (icao) * FROM {} SELECT DISTINCT ON (icao) * FROM {}
@@ -1101,10 +1111,10 @@ impl Metar {
} }
pub async fn get_or_update_metars( pub async fn get_or_update_metars(
client: &HttpClient, state: &AppState,
icaos: &Vec<String>, icaos: &Vec<String>,
) -> ApiResult<Vec<Self>> { ) -> 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 current_time = Utc::now().timestamp();
let mut updated_metars: Vec<Self> = vec![]; let mut updated_metars: Vec<Self> = vec![];
@@ -1120,7 +1130,7 @@ impl Metar {
// Handle outdated METARs // Handle outdated METARs
if current_time > (metar.observation_time.timestamp() + time_offset()) { if current_time > (metar.observation_time.timestamp() + time_offset()) {
// If the METAR has previously been found, get the updated_at time, otherwise default // 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(), Some(c) => current_time - c.updated_at.timestamp(),
None => DEFAULT_REFRESH_DURATION, None => DEFAULT_REFRESH_DURATION,
}; };
@@ -1143,15 +1153,15 @@ impl Metar {
// Otherwise add the valid metar to the updated list // Otherwise add the valid metar to the updated list
else { else {
found_metar_icaos.insert(icao.clone()); found_metar_icaos.insert(icao.clone());
let metar_check = MetarCheck::new(icao, true).await; let metar_check = MetarCheck::new(state, icao, true).await;
metar_check.insert().await?; metar_check.insert(state).await?;
updated_metars.push(metar); updated_metars.push(metar);
} }
} }
// Add all METARs that were not in the returned database METARs // Add all METARs that were not in the returned database METARs
for icao in &requested_icaos { for icao in &requested_icaos {
match MetarCheck::get(icao).await { match MetarCheck::get(state, icao).await {
Some(c) => { Some(c) => {
if current_time > (c.updated_at.timestamp() + DEFAULT_REFRESH_DURATION) { if current_time > (c.updated_at.timestamp() + DEFAULT_REFRESH_DURATION) {
missing_metar_icaos.push(icao.to_string()); missing_metar_icaos.push(icao.to_string());
@@ -1169,7 +1179,7 @@ impl Metar {
"Retrieving missing METAR data for {:?}", "Retrieving missing METAR data for {:?}",
missing_metar_icaos 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 .await
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
log::warn!("Unable to get remote METAR data; {}", err); log::warn!("Unable to get remote METAR data; {}", err);
@@ -1179,19 +1189,19 @@ impl Metar {
// Insert missing METARs // Insert missing METARs
if remote_metars.len() > 0 { if remote_metars.len() > 0 {
for remote_metar in remote_metars.clone() { 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()); 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.last_metar = Some(remote_metar);
metar_check.insert().await?; metar_check.insert(state).await?;
} }
updated_metars.append(&mut remote_metars); updated_metars.append(&mut remote_metars);
} }
// Update still missing METARs // Update still missing METARs
for difference in found_metar_icaos.symmetric_difference(&requested_icaos) { for difference in found_metar_icaos.symmetric_difference(&requested_icaos) {
let metar_check = MetarCheck::new(difference.to_string(), false).await; let metar_check = MetarCheck::new(state, difference.to_string(), false).await;
metar_check.insert().await?; metar_check.insert(state).await?;
// Only add cached metar data if it's less than 4 hours old // Only add cached metar data if it's less than 4 hours old
if let Some(last_metar) = metar_check.last_metar { if let Some(last_metar) = metar_check.last_metar {
let four_hours_ago = Utc::now() - chrono::Duration::hours(4); let four_hours_ago = Utc::now() - chrono::Duration::hours(4);
@@ -1205,26 +1215,26 @@ impl Metar {
Ok(updated_metars) Ok(updated_metars)
} }
pub async fn update_metars(client: &HttpClient, etag: Option<String>) -> ApiResult<String> { pub async fn update_metars(state: &AppState, etag: Option<String>) -> ApiResult<String> {
let (remote_metars, etag) = Self::get_cached_remote_metars(client, etag) let (remote_metars, etag) = Self::get_cached_remote_metars(state, etag)
.await .await
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
log::warn!("Unable to get cached remote METAR data; {}", err); log::warn!("Unable to get cached remote METAR data; {}", err);
(vec![], String::new()) (vec![], String::new())
}); });
MetarRow::insert_all(remote_metars).await?; MetarRow::insert_all(&state.pool, remote_metars).await?;
Ok(etag) Ok(etag)
} }
pub async fn insert(&self) -> ApiResult<()> { pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
log::trace!( log::trace!(
"Inserting metar {} with observation time {}", "Inserting metar {} with observation time {}",
self.icao, self.icao,
self.observation_time self.observation_time
); );
let metar: MetarRow = self.to_row()?; let metar: MetarRow = self.to_row()?;
metar.insert().await?; metar.insert(pool).await?;
Ok(()) Ok(())
} }
} }
@@ -1235,43 +1245,45 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_metar_parse() { async fn test_metar_parse() {
let state = AppState::new().await.unwrap();
let mut metar_string = "METAR KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT \ 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 \ -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 \ RWY11 RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 \
TSNO $" TSNO $"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
metar_string = "KMIA 090053Z 33004KT 10SM FEW015 FEW024 SCT075 SCT250 25/22 A2990 RMK AO2 \ metar_string = "KMIA 090053Z 33004KT 10SM FEW015 FEW024 SCT075 SCT250 25/22 A2990 RMK AO2 \
SLP126 T02500217 $" SLP126 T02500217 $"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
metar_string = metar_string =
"KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117" "KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
metar_string = "KHEF 092356Z 13009KT 10SM CLR 08/M03 A3022 RMK AO2 SLP239 6//// T00831033 \ metar_string = "KHEF 092356Z 13009KT 10SM CLR 08/M03 A3022 RMK AO2 SLP239 6//// T00831033 \
10133 20078 53002 PNO $" 10133 20078 53002 PNO $"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
metar_string = "KSLK 162351Z AUTO VRB03KT 1SM -SN BR FEW007 OVC014 00/M02 A2974 RMK AO2 \ metar_string = "KSLK 162351Z AUTO VRB03KT 1SM -SN BR FEW007 OVC014 00/M02 A2974 RMK AO2 \
SLP090 P0001 60004 T00001017 10000 21011 53026" SLP090 P0001 60004 T00001017 10000 21011 53026"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
metar_string = "KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT -RA BR BKN015 OVC025 \ 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 \ 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 $" RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 TSNO $"
.to_string(); .to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time); dbg!(&metar.observation_time);
dbg!(&metar.sky_condition); dbg!(&metar.sky_condition);
} }

View File

@@ -24,7 +24,7 @@ struct MetarQuery {
), ),
)] )]
#[get("")] #[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 parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
let icao_option = &parameters.icaos; let icao_option = &parameters.icaos;
if let None = icao_option { 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 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, Ok(a) => a,
Err(err) => { Err(err) => {
error!("{}", err); error!("{}", err);
@@ -61,8 +61,8 @@ async fn find_all(req: HttpRequest) -> HttpResponse {
) )
)] )]
#[put("")] #[put("")]
async fn refresh_metars(data: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse { async fn refresh_metars(state: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse {
let client = data.client.clone(); let client = state.client.clone();
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap(); let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
let icao_option = &parameters.icaos; let icao_option = &parameters.icaos;
if let None = icao_option { 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 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, Ok(a) => a,
Err(err) => { Err(err) => {
error!("{}", err); error!("{}", err);

View File

@@ -1,19 +1,13 @@
use crate::http_client::HttpClient;
use crate::metars::Metar; use crate::metars::Metar;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::env; use std::env;
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::interval; use tokio::time::interval;
use crate::state::AppState;
pub fn run() { pub fn run(state: Arc<AppState>) {
tokio::spawn(async { tokio::spawn(async move {
let client = match HttpClient::default() {
Ok(client) => client,
Err(err) => {
log::error!("Failed to create HTTP client: {}", err);
return;
}
};
let seconds = env::var("METAR_INTERVAL") let seconds = env::var("METAR_INTERVAL")
.unwrap_or("300".to_string()) .unwrap_or("300".to_string())
.parse::<u64>() .parse::<u64>()
@@ -32,7 +26,7 @@ pub fn run() {
log::debug!("METAR update started at {}", start_utc); log::debug!("METAR update started at {}", start_utc);
// Run the update // 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), Ok(new_etag) => etag = Some(new_etag),
Err(err) => log::error!("METAR update failed: {}", err), Err(err) => log::error!("METAR update failed: {}", err),
} }

167
api/src/state.rs Normal file
View 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 {}", &region.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
View 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,
}

View File

@@ -25,7 +25,7 @@ services:
volumes: volumes:
- ./ssl:/etc/nginx/ssl/ - ./ssl:/etc/nginx/ssl/
networks: networks:
- default - web
<<: *default_restart <<: *default_restart
postgres: postgres:
@@ -37,36 +37,32 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- /data/aviation/postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
- /data/aviation/postgres_logs:/var/log - postgres_logs:/var/log
ports: ports:
- "${POSTGRES_PORT:-5432}:5432" - "${POSTGRES_PORT:-5432}:5432"
networks:
- default
profiles: profiles:
- backend - backend
<<: *default_restart <<: *default_restart
redis: valkey:
image: gitea.bensherriff.com/homelab/redis:8.0-M03 image: valkey/valkey:8.1.3
container_name: aviation-redis container_name: aviation-valkey
volumes: volumes:
- redis:/data - valkey:/data
ports: ports:
- "${REDIS_PORT:-6379}:6379" - "${VALKEY_PORT:-6379}:6379"
healthcheck: healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] test: [ "CMD", "valkey-cli", "--raw", "incr", "ping" ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
networks:
- default
profiles: profiles:
- backend - backend
<<: *default_restart <<: *default_restart
minio: 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 container_name: aviation-minio
environment: environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_USER: ${MINIO_ROOT_USER}
@@ -78,8 +74,6 @@ services:
ports: ports:
- "${MINIO_PORT:-9000}:9000" - "${MINIO_PORT:-9000}:9000"
- "${MINIO_INTERNAL_PORT:-9001}:9001" - "${MINIO_INTERNAL_PORT:-9001}:9001"
networks:
- default
profiles: profiles:
- backend - backend
command: server --console-address ":9001" /data command: server --console-address ":9001" /data
@@ -97,8 +91,8 @@ services:
API_PORT: 5000 API_PORT: 5000
POSTGRES_HOST: aviation-postgres POSTGRES_HOST: aviation-postgres
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
REDIS_HOST: aviation-redis VALKEY_HOST: aviation-valkey
REDIS_PORT: 6379 VALKEY_PORT: 6379
MINIO_HOST: aviation-minio MINIO_HOST: aviation-minio
MINIO_PORT: 9000 MINIO_PORT: 9000
TEMPLATE_DIR: /templates TEMPLATE_DIR: /templates
@@ -109,36 +103,12 @@ services:
- "${API_PORT:-5000}:5000" - "${API_PORT:-5000}:5000"
depends_on: depends_on:
- postgres - postgres
- redis - valkey
- minio - minio
networks:
- default
profiles: profiles:
- api - api
<<: *default_restart <<: *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: mailpit:
image: axllent/mailpit image: axllent/mailpit
container_name: mailpit container_name: mailpit
@@ -152,16 +122,17 @@ services:
- "${MAILPIT_SMTP_PORT:-1025}:1025" - "${MAILPIT_SMTP_PORT:-1025}:1025"
volumes: volumes:
- mailpit:/data - mailpit:/data
networks:
- default
profiles: profiles:
- dev - dev
<<: *default_restart <<: *default_restart
volumes: volumes:
redis: postgres:
postgres_logs:
valkey:
minio: minio:
mailpit: mailpit:
networks: networks:
default: web:
driver: bridge

View File

@@ -4,31 +4,33 @@ worker_processes auto;
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;
sendfile on; # allow HTTP/2 on the frontend
#tcp_nopush on; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
keepalive_timeout 65; sendfile on;
tcp_nopush on;
#gzip on; keepalive_timeout 65;
# Set client limit to 100 MB gzip on;
client_max_body_size 100M;
include /etc/nginx/conf.d/*.conf; # Set client limit to 100 MB
client_max_body_size 100M;
include /etc/nginx/conf.d/*.conf;
} }

View File

@@ -1,12 +1,12 @@
/* Set up Flexbox layout */ html, body, #root {
.App {
display: flex;
flex-direction: column;
height: 100%; height: 100%;
} }
.map-wrapper { .map-wrapper {
display: flex;
flex-direction: column;
flex: 1; flex: 1;
min-height: 0;
} }
.leaflet-container { .leaflet-container {

View File

@@ -6,7 +6,6 @@ import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png'; import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import L from 'leaflet'; import L from 'leaflet';
import { Header } from '@components/Header';
import AirportLayer from '@components/AirportLayer.tsx'; import AirportLayer from '@components/AirportLayer.tsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Airport } from '@lib/airport.types.ts'; 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 { GroupControl } from '@components/GroupControl.tsx';
import { AirportDrawer } from '@components/AirportDrawer'; import { AirportDrawer } from '@components/AirportDrawer';
import { LocateControl } from '@components/LocateControl.tsx'; import { LocateControl } from '@components/LocateControl.tsx';
import { Footer } from '@components/Footer';
// Fix Leaflet's default icon path issues with Webpack // Fix Leaflet's default icon path issues with Webpack
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
@@ -91,57 +89,54 @@ function App() {
} }
return ( return (
<div className='App'> <div className='map-wrapper' style={{ flex: 1, minHeight: 0 }}>
<Header /> <MapContainer
<div className='map-wrapper'> style={{ height: '100%', width: '100%' }}
<MapContainer className='leaflet-container'
className='leaflet-container' attributionControl={false}
attributionControl={false} center={defaultCenter}
center={defaultCenter} zoom={defaultZoom}
zoom={defaultZoom} minZoom={3}
minZoom={3} maxZoom={19}
maxZoom={19} maxBounds={[
maxBounds={[ [-85.06, -181],
[-85.06, -181], [85.06, 181]
[85.06, 181] ]}
scrollWheelZoom={true}
zoomControl={false}
markerZoomAnimation={false}
>
<AirportDrawer airport={airport} setAirport={setAirport} />
<LayersControl>
{layerMap.map((layer, index) => (
<LayersControl.BaseLayer key={index} checked={selectedLayerIndex === `${index}`} name={layer.name}>
<TileLayer url={layer.url} />
</LayersControl.BaseLayer>
))}
</LayersControl>
<ScaleControl />
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={10} />}
<ZoomControl position={'bottomright'} />
<AirportLayer setAirport={setAirport} selectedLayer={selectedLayer} showNoMetar={showNoMetar} />
<BaseLayerChangeHandler />
<LocateControl />
<GroupControl
buttons={[
{
title: 'Toggle radar',
active: showRadar,
onClick: toggleRadar,
icon: <IconRadar />
},
{
title: 'Toggle nonMETAR airports',
active: showNoMetar,
onClick: toggleShowNoMetar,
icon: <IconBuildingAirport />
}
]} ]}
scrollWheelZoom={true} />
zoomControl={false} </MapContainer>
markerZoomAnimation={false}
>
<AirportDrawer airport={airport} setAirport={setAirport} />
<LayersControl>
{layerMap.map((layer, index) => (
<LayersControl.BaseLayer key={index} checked={selectedLayerIndex === `${index}`} name={layer.name}>
<TileLayer url={layer.url} />
</LayersControl.BaseLayer>
))}
</LayersControl>
<ScaleControl />
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={10} />}
<ZoomControl position={'bottomright'} />
<AirportLayer setAirport={setAirport} selectedLayer={selectedLayer} showNoMetar={showNoMetar} />
<BaseLayerChangeHandler />
<LocateControl />
<GroupControl
buttons={[
{
title: 'Toggle radar',
active: showRadar,
onClick: toggleRadar,
icon: <IconRadar />
},
{
title: 'Toggle nonMETAR airports',
active: showNoMetar,
onClick: toggleShowNoMetar,
icon: <IconBuildingAirport />
}
]}
/>
</MapContainer>
</div>
<Footer />
</div> </div>
); );
} }

View File

@@ -1,4 +1,3 @@
import { Header } from '@components/Header';
import { useUserContext } from '@components/context/UserContext.tsx'; import { useUserContext } from '@components/context/UserContext.tsx';
import { AirportTable } from '@components/AirportTable'; import { AirportTable } from '@components/AirportTable';
import { AirportDrop } from '@components/AirportDrop'; import { AirportDrop } from '@components/AirportDrop';
@@ -13,7 +12,6 @@ export function Administration() {
return ( return (
<> <>
<Header />
<AirportTable /> <AirportTable />
<AirportDrop /> <AirportDrop />
</> </>

View File

@@ -71,7 +71,7 @@ export function AirportDrawer({
onClose={() => setAirport(null)} onClose={() => setAirport(null)}
withinPortal withinPortal
zIndex={1000} 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' padding='md'
size={isMobile ? '100%' : 'md'} size={isMobile ? '100%' : 'md'}
position='left' position='left'

View File

@@ -1,9 +1,11 @@
.footer { .footer {
background: #32495f; height: 48px;
background: #2b2d31;
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
} }
.inner { .inner {
height: 48px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@@ -40,7 +40,7 @@ export function Footer() {
</a> </a>
</Text> </Text>
<Divider orientation={'vertical'} /> <Divider orientation={'vertical'} />
<Text size='sm'>© {new Date().getFullYear()} Aviation Data</Text> <Text size='sm'>© {new Date().getFullYear()} bensherriff.com</Text>
</Group> </Group>
{!isMobile && ( {!isMobile && (
<Group gap='xs' justify='flex-end' wrap='nowrap'> <Group gap='xs' justify='flex-end' wrap='nowrap'>

View File

@@ -2,7 +2,7 @@
height: 56px; height: 56px;
padding: 0 16px 0 16px; padding: 0 16px 0 16px;
/*background-color: var(--mantine-color-body);*/ /*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)); border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
} }

View File

@@ -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 { useDisclosure, useMediaQuery, useToggle } from '@mantine/hooks';
import classes from './Header.module.css'; import classes from './Header.module.css';
import { HeaderModal } from '@components/Header/HeaderModal.tsx'; import { HeaderModal } from '@components/Header/HeaderModal.tsx';
@@ -146,7 +146,7 @@ export function Header() {
<Group justify='space-between' h='100%'> <Group justify='space-between' h='100%'>
<Group align='center' gap='xs'> <Group align='center' gap='xs'>
<Link to='/'> <Link to='/'>
<Avatar src='/logo.svg' alt='logo' onClick={toggle} /> <Avatar src='/logo.svg' alt='logo' />
</Link> </Link>
<Text size={'xl'}>Aviation Data</Text> <Text size={'xl'}>Aviation Data</Text>
</Group> </Group>
@@ -173,6 +173,21 @@ export function Header() {
</Group> </Group>
</header> </header>
</Box> </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} /> <HeaderModal type={modalType} toggle={modalToggle} login={loginUser} register={registerUser} />
</> </>
); );

View 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>
);
}

View File

@@ -1,4 +1,3 @@
import { Header } from '@components/Header';
import { useUserContext } from '@components/context/UserContext.tsx'; import { useUserContext } from '@components/context/UserContext.tsx';
import { NotFound } from '@components/NotFound'; import { NotFound } from '@components/NotFound';
@@ -11,7 +10,6 @@ export function Profile() {
return ( return (
<> <>
<Header />
Todo: profile {user?.firstName} Todo: profile {user?.firstName}
</> </>
); );

View File

@@ -1,3 +1,4 @@
import 'leaflet/dist/leaflet.css';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import './index.css'; import './index.css';
@@ -9,6 +10,7 @@ import { BrowserRouter, Route, Routes } from 'react-router';
import { Profile } from '@components/Profile.tsx'; import { Profile } from '@components/Profile.tsx';
import { Administration } from '@components/Administration.tsx'; import { Administration } from '@components/Administration.tsx';
import { NotFound } from '@components/NotFound'; import { NotFound } from '@components/NotFound';
import { MainLayout } from '@components/MainLayout.tsx';
const theme = createTheme({ const theme = createTheme({
fontFamily: 'Inter, sans-serif' fontFamily: 'Inter, sans-serif'
@@ -21,9 +23,11 @@ createRoot(document.getElementById('root')!).render(
<Notifications zIndex={2000} /> <Notifications zIndex={2000} />
<UserProvider> <UserProvider>
<Routes> <Routes>
<Route path='/' element={<App />} /> <Route path="/" element={<MainLayout />}>
<Route path='/profile' element={<Profile />} /> <Route index element={<App />} />
<Route path='/administration' element={<Administration />} /> <Route path='profile' element={<Profile />} />
<Route path='administration' element={<Administration />} />
</Route>
<Route path='*' element={<NotFound />} /> <Route path='*' element={<NotFound />} />
</Routes> </Routes>
</UserProvider> </UserProvider>