Refactor to break out scheduler
This commit is contained in:
4775
Cargo.lock
generated
Normal file
4775
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
Cargo.toml
Normal file
4
Cargo.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[workspace]
|
||||||
|
|
||||||
|
members = [ "crates/adsb", "crates/api", "crates/lib", "crates/scheduler" ]
|
||||||
|
resolver = "2"
|
||||||
115
Taskfile.yml
115
Taskfile.yml
@@ -4,6 +4,28 @@ version: '3'
|
|||||||
|
|
||||||
dotenv: ['.env.local', '.env']
|
dotenv: ['.env.local', '.env']
|
||||||
|
|
||||||
|
vars:
|
||||||
|
version: '{{ coalesce .version .v "latest" }}'
|
||||||
|
folder: '{{ coalesce .folder .f "nginx" }}'
|
||||||
|
registry: '{{ coalesce .registry .r "gitea.bensherriff.com/bsherriff" }}'
|
||||||
|
platform: '{{ coalesce .platform .p "linux/amd64,linux/arm64" }}'
|
||||||
|
image: '{{.registry}}/aviation-{{.folder}}:{{.version}}'
|
||||||
|
build_date:
|
||||||
|
sh: date -u +%Y-%m-%dT%H:%M:%SZ
|
||||||
|
vcs_ref:
|
||||||
|
sh: git rev-parse HEAD
|
||||||
|
context: '{{ coalesce .context .ctx "." }}'
|
||||||
|
dockerfile: >
|
||||||
|
{{- if or (eq .folder "nginx") (eq .folder "ui") -}}
|
||||||
|
{{.folder}}/Dockerfile
|
||||||
|
{{- else if eq .folder "api" -}}
|
||||||
|
crates/api/Dockerfile
|
||||||
|
{{- else if eq .folder "scheduler" -}}
|
||||||
|
crates/scheduler/Dockerfile
|
||||||
|
{{- else -}}
|
||||||
|
{{ fail (printf "Invalid folder '%s'. Valid: nginx, ui, api, scheduler" .folder) }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
cmds:
|
cmds:
|
||||||
@@ -18,66 +40,101 @@ tasks:
|
|||||||
dev-servers:
|
dev-servers:
|
||||||
deps:
|
deps:
|
||||||
- task: run-api
|
- task: run-api
|
||||||
|
- task: run-scheduler
|
||||||
- task: run-ui
|
- task: run-ui
|
||||||
|
|
||||||
# API Commands
|
# API Commands
|
||||||
build-api:
|
build-api:
|
||||||
dir: api
|
dir: crates/api
|
||||||
cmds:
|
cmd: cargo build
|
||||||
- cargo build
|
|
||||||
format-api:
|
format-api:
|
||||||
dir: api
|
dir: crates/api
|
||||||
cmds:
|
cmd: cargo fmt
|
||||||
- cargo fmt
|
|
||||||
run-api:
|
run-api:
|
||||||
dir: api
|
dir: crates/api
|
||||||
cmds:
|
cmd: cargo run
|
||||||
- cargo run
|
silent: true
|
||||||
|
|
||||||
|
# Scheduler Commands
|
||||||
|
build-scheduler:
|
||||||
|
dir: crates/scheduler
|
||||||
|
cmd: cargo build
|
||||||
|
format-scheduler:
|
||||||
|
dir: crates/scheduler
|
||||||
|
cmd: cargo fmt
|
||||||
|
run-scheduler:
|
||||||
|
dir: crates/scheduler
|
||||||
|
cmd: cargo run
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
# UI Commands
|
# UI Commands
|
||||||
build-ui:
|
build-ui:
|
||||||
dir: ui
|
dir: ui
|
||||||
cmds:
|
cmd: npm run build
|
||||||
- npm run build
|
|
||||||
format-ui:
|
format-ui:
|
||||||
dir: ui
|
dir: ui
|
||||||
cmds:
|
cmd: npm run format
|
||||||
- npm run format
|
|
||||||
clean-ui:
|
clean-ui:
|
||||||
dir: ui
|
dir: ui
|
||||||
cmds:
|
cmd: rm -rf node_modules dist stats.html
|
||||||
- rm -rf node_modules dist stats.html
|
|
||||||
run-ui:
|
run-ui:
|
||||||
dir: ui
|
dir: ui
|
||||||
cmds:
|
cmd: npm run dev
|
||||||
- npm run dev
|
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
# Docker Commands
|
# Docker Commands
|
||||||
docker-backend:
|
docker-backend:
|
||||||
cmds:
|
cmd: docker compose --profile backend up -d
|
||||||
- docker compose --profile backend up -d
|
|
||||||
docker-up:
|
docker-up:
|
||||||
cmds:
|
cmd: docker compose --profile backend --profile api up -d
|
||||||
- docker compose --profile backend --profile api up -d
|
|
||||||
docker-down:
|
docker-down:
|
||||||
cmds:
|
cmd: docker compose --profile backend --profile api down
|
||||||
- docker compose --profile backend --profile api down
|
|
||||||
docker-clean:
|
docker-clean:
|
||||||
cmds:
|
cmd: docker compose --profile backend --profile api down -v
|
||||||
- docker compose --profile backend --profile api down -v
|
|
||||||
docker-refresh:
|
docker-refresh:
|
||||||
cmds:
|
cmds:
|
||||||
- task: docker-clean
|
- task: docker-clean
|
||||||
- task: docker-up
|
- task: docker-up
|
||||||
docker-build:
|
|
||||||
|
build:
|
||||||
|
desc: Build a specific docker image from a folder
|
||||||
cmds:
|
cmds:
|
||||||
- docker compose build
|
- |
|
||||||
|
docker buildx build \
|
||||||
|
-f {{.dockerfile}} \
|
||||||
|
-t {{.image}} \
|
||||||
|
--load \
|
||||||
|
--build-arg BUILD_DATE={{.build_date}} \
|
||||||
|
--build-arg BUILD_VERSION={{.version}} \
|
||||||
|
--build-arg VCS_REF={{.vcs_ref}} \
|
||||||
|
{{.context}}
|
||||||
|
|
||||||
|
push:
|
||||||
|
desc: Build and push a specific docker image from a folder
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
docker buildx create \
|
||||||
|
--use \
|
||||||
|
--driver docker-container \
|
||||||
|
--bootstrap \
|
||||||
|
--name default-builder \
|
||||||
|
--platform {{.platform}} \
|
||||||
|
|| true
|
||||||
|
ignore_error: true
|
||||||
|
- |
|
||||||
|
docker buildx build \
|
||||||
|
-f {{.dockerfile}} \
|
||||||
|
--platform {{.platform}} \
|
||||||
|
-t {{.image}} \
|
||||||
|
--push \
|
||||||
|
--build-arg BUILD_DATE={{.build_date}} \
|
||||||
|
--build-arg BUILD_VERSION={{.version}} \
|
||||||
|
--build-arg VCS_REF={{.vcs_ref}} \
|
||||||
|
{{.context}}
|
||||||
|
.
|
||||||
|
|
||||||
psql:
|
psql:
|
||||||
cmds:
|
cmd: docker exec -it aviation-postgres psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -P pager=off
|
||||||
- docker exec -it aviation-postgres psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -P pager=off
|
|
||||||
|
|
||||||
cert:
|
cert:
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod model;
|
|
||||||
pub mod routes;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use routes::init_routes;
|
|
||||||
245
api/src/error.rs
245
api/src/error.rs
@@ -1,245 +0,0 @@
|
|||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::{HttpResponse, ResponseError};
|
|
||||||
use log::warn;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::fmt;
|
|
||||||
use std::sync::{MutexGuard, PoisonError};
|
|
||||||
|
|
||||||
pub type ApiResult<T> = Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Error {
|
|
||||||
pub status: u16,
|
|
||||||
pub details: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn new(status: u16, details: String) -> Self {
|
|
||||||
Self {
|
|
||||||
status,
|
|
||||||
details,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_http_response(&self) -> HttpResponse {
|
|
||||||
let status = StatusCode::from_u16(self.status).unwrap_or_else(|err| {
|
|
||||||
warn!("{}", err);
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
});
|
|
||||||
HttpResponse::build(status).body(self.details.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str(self.details.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {
|
|
||||||
fn description(&self) -> &str {
|
|
||||||
&self.details
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseError for Error {
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
|
||||||
let status =
|
|
||||||
StatusCode::from_u16(self.status).unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
|
|
||||||
let status_code = status.as_u16();
|
|
||||||
let details = match status_code {
|
|
||||||
401 => String::from("Unauthorized"),
|
|
||||||
code if code < 500 => self.details.clone(),
|
|
||||||
_ => {
|
|
||||||
log::error!("Internal server error: {}", self.details);
|
|
||||||
String::from("Internal Server Error")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse::build(status).json(json!({ "status": status_code, "details": details }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(error: std::io::Error) -> Self {
|
|
||||||
Self::new(500, format!("Unknown IO error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<chrono::ParseError> for Error {
|
|
||||||
fn from(error: chrono::ParseError) -> Self {
|
|
||||||
Self::new(500, format!("Chrono parse error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<core::num::ParseIntError> for Error {
|
|
||||||
fn from(error: core::num::ParseIntError) -> Self {
|
|
||||||
Self::new(500, format!("Integer parse error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<core::num::ParseFloatError> for Error {
|
|
||||||
fn from(error: core::num::ParseFloatError) -> Self {
|
|
||||||
Self::new(500, format!("Float parse error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::env::VarError> for Error {
|
|
||||||
fn from(error: std::env::VarError) -> Self {
|
|
||||||
Self::new(
|
|
||||||
500,
|
|
||||||
format!("Unknown environment variable error: {:?}", error),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
|
||||||
fn from(error: reqwest::Error) -> Self {
|
|
||||||
match error.status() {
|
|
||||||
Some(status_code) => {
|
|
||||||
if status_code.is_client_error() {
|
|
||||||
Self::new(500, format!("Client reqwest error: {:?}", error))
|
|
||||||
} else if status_code.is_server_error() {
|
|
||||||
Self::new(500, format!("Server reqwest error: {:?}", error))
|
|
||||||
} else {
|
|
||||||
Self::new(500, format!("Unknown reqwest error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Self::new(500, format!("Unknown reqwest error: {:?}", error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
|
||||||
fn from(error: serde_json::Error) -> Self {
|
|
||||||
Self::new(500, format!("Unknown serde_json error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<argon2::password_hash::Error> for Error {
|
|
||||||
fn from(error: argon2::password_hash::Error) -> Self {
|
|
||||||
Self::new(500, format!("Unknown argon2 error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<redis::RedisError> for Error {
|
|
||||||
fn from(error: redis::RedisError) -> Self {
|
|
||||||
Self::new(500, format!("Unknown redis error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<s3::error::S3Error> for Error {
|
|
||||||
fn from(error: s3::error::S3Error) -> Self {
|
|
||||||
match error {
|
|
||||||
s3::error::S3Error::Credentials(err) => {
|
|
||||||
Self::new(500, format!("Unknown s3 credentials error: {:?}", err))
|
|
||||||
}
|
|
||||||
s3::error::S3Error::FromUtf8(err) => {
|
|
||||||
Self::new(500, format!("Unknown s3 from utf8 error: {:?}", err))
|
|
||||||
}
|
|
||||||
s3::error::S3Error::FmtError(err) => {
|
|
||||||
Self::new(500, format!("Unknown s3 fmt error: {:?}", err))
|
|
||||||
}
|
|
||||||
s3::error::S3Error::HeaderToStr(err) => {
|
|
||||||
Self::new(500, format!("Unknown s3 header to str error: {:?}", err))
|
|
||||||
}
|
|
||||||
s3::error::S3Error::HmacInvalidLength(err) => Self::new(
|
|
||||||
500,
|
|
||||||
format!("Unknown s3 hmac invalid length error: {:?}", err),
|
|
||||||
),
|
|
||||||
s3::error::S3Error::Http(error) => Self::new(error.status_code().as_u16(), error.to_string()),
|
|
||||||
_ => {
|
|
||||||
let re = Regex::new(r"HTTP (\d{3})").unwrap();
|
|
||||||
// Apply the regex to the input string
|
|
||||||
if let Some(captures) = re.captures(&error.to_string()) {
|
|
||||||
if let Some(http_code_str) = captures.get(1) {
|
|
||||||
if let Ok(http_code) = http_code_str.as_str().parse::<u16>() {
|
|
||||||
return Self::new(http_code, error.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::new(500, format!("Unknown s3 error: {:?}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<sqlx::Error> for Error {
|
|
||||||
fn from(error: sqlx::Error) -> Self {
|
|
||||||
match error {
|
|
||||||
sqlx::Error::RowNotFound => Error::new(404, "Not found".to_string()),
|
|
||||||
sqlx::Error::ColumnIndexOutOfBounds { .. } => Error::new(422, error.to_string()),
|
|
||||||
sqlx::Error::ColumnNotFound { .. } => Error::new(422, error.to_string()),
|
|
||||||
sqlx::Error::ColumnDecode { .. } => Error::new(422, error.to_string()),
|
|
||||||
sqlx::Error::Decode(_) => Error::new(422, error.to_string()),
|
|
||||||
sqlx::Error::PoolTimedOut => Error::new(503, error.to_string()),
|
|
||||||
sqlx::Error::PoolClosed => Error::new(503, error.to_string()),
|
|
||||||
sqlx::Error::Tls(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::Io(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::Protocol(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::Configuration(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::AnyDriverError(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::Database(err) => {
|
|
||||||
if let Some(code) = err.code() {
|
|
||||||
match code.trim() {
|
|
||||||
// Unique violation
|
|
||||||
"23505" => return Error::new(409, err.to_string()),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Error::new(500, err.to_string())
|
|
||||||
}
|
|
||||||
sqlx::Error::Migrate(_) => Error::new(500, error.to_string()),
|
|
||||||
sqlx::Error::TypeNotFound { type_name } => {
|
|
||||||
Error::new(500, format!("Type not found: {}", type_name))
|
|
||||||
}
|
|
||||||
sqlx::Error::WorkerCrashed => Error::new(500, error.to_string()),
|
|
||||||
_ => Error::new(500, error.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<sqlx::migrate::MigrateError> for Error {
|
|
||||||
fn from(error: sqlx::migrate::MigrateError) -> Self {
|
|
||||||
Error::new(500, error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lettre::address::AddressError> for Error {
|
|
||||||
fn from(error: lettre::address::AddressError) -> Self {
|
|
||||||
Error::new(500, error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lettre::error::Error> for Error {
|
|
||||||
fn from(error: lettre::error::Error) -> Self {
|
|
||||||
Error::new(500, error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lettre::transport::smtp::Error> for Error {
|
|
||||||
fn from(error: lettre::transport::smtp::Error) -> Self {
|
|
||||||
Error::new(500, error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Error {
|
|
||||||
fn from(error: String) -> Self {
|
|
||||||
Self::new(500, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<regex::Error> for Error {
|
|
||||||
fn from(error: regex::Error) -> Self {
|
|
||||||
Self::new(500, error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> From<PoisonError<MutexGuard<'a, T>>> for Error {
|
|
||||||
fn from(_: PoisonError<MutexGuard<'a, T>>) -> Self {
|
|
||||||
Self::new(500, "Failed to acquire lock".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
use crate::metars::Metar;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use std::env;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tokio::time::interval;
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
pub fn run(state: Arc<AppState>) {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let seconds = env::var("METAR_INTERVAL")
|
|
||||||
.unwrap_or("300".to_string())
|
|
||||||
.parse::<u64>()
|
|
||||||
.unwrap_or(300);
|
|
||||||
|
|
||||||
// Create an interval ticker
|
|
||||||
let mut interval = interval(Duration::from_secs(seconds));
|
|
||||||
let mut etag = None;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
|
|
||||||
// Record start times
|
|
||||||
let start_monotonic = Instant::now();
|
|
||||||
let start_utc: DateTime<Utc> = Utc::now();
|
|
||||||
log::debug!("METAR update started at {}", start_utc);
|
|
||||||
|
|
||||||
// Run the update
|
|
||||||
match Metar::update_metars(&state, etag.clone()).await {
|
|
||||||
Ok(new_etag) => etag = Some(new_etag),
|
|
||||||
Err(err) => log::error!("METAR update failed: {}", err),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed = start_monotonic.elapsed();
|
|
||||||
let next_utc = Utc::now() + chrono::Duration::from_std(Duration::from_secs(seconds)).unwrap();
|
|
||||||
log::info!(
|
|
||||||
"METAR update finished in {:.2?}; next run at {}",
|
|
||||||
elapsed,
|
|
||||||
next_utc
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Change Password
|
name: Change Password
|
||||||
type: http
|
type: http
|
||||||
seq: 6
|
seq: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
put {
|
put {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Confirm Password Reset
|
name: Confirm Password Reset
|
||||||
type: http
|
type: http
|
||||||
seq: 8
|
seq: 9
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Get Profile
|
name: Get Profile
|
||||||
type: http
|
type: http
|
||||||
seq: 10
|
seq: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
18
bruno/Account/Login Admin (Default).bru
Normal file
18
bruno/Account/Login Admin (Default).bru
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
meta {
|
||||||
|
name: Login Admin (Default)
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{API_URL}}/account/login
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "changeme"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Logout
|
name: Logout
|
||||||
type: http
|
type: http
|
||||||
seq: 5
|
seq: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Refresh Session
|
name: Refresh Session
|
||||||
type: http
|
type: http
|
||||||
seq: 9
|
seq: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Reset Password
|
name: Reset Password
|
||||||
type: http
|
type: http
|
||||||
seq: 7
|
seq: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
0
adsb/Cargo.lock → crates/adsb/Cargo.lock
generated
0
adsb/Cargo.lock → crates/adsb/Cargo.lock
generated
301
api/Cargo.lock → crates/api/Cargo.lock
generated
301
api/Cargo.lock → crates/api/Cargo.lock
generated
@@ -444,10 +444,11 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attohttpc"
|
name = "attohttpc"
|
||||||
version = "0.28.5"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3"
|
checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"log",
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
@@ -464,9 +465,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-creds"
|
name = "aws-creds"
|
||||||
version = "0.37.0"
|
version = "0.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f84143206b9c72b3c5cb65415de60c7539c79cd1559290fddec657939131be0"
|
checksum = "b13804829a843b3f26e151c97acbb315ee1177a2724690edfcd28f1894146200"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"attohttpc",
|
"attohttpc",
|
||||||
"home",
|
"home",
|
||||||
@@ -474,18 +475,18 @@ dependencies = [
|
|||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-region"
|
name = "aws-region"
|
||||||
version = "0.25.5"
|
version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b"
|
checksum = "5532f65342f789f9c1b7078ea9c9cd9293cd62dcc284fa99adc4a1c9ba43469c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -599,6 +600,15 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.23"
|
version = "1.2.23"
|
||||||
@@ -660,6 +670,19 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1362,7 +1385,7 @@ dependencies = [
|
|||||||
"pest_derive",
|
"pest_derive",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1467,17 +1490,6 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "http-body"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"http 0.2.12",
|
|
||||||
"pin-project-lite",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -1497,7 +1509,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1513,29 +1525,6 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper"
|
|
||||||
version = "0.14.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
|
||||||
"http 0.2.12",
|
|
||||||
"http-body 0.4.6",
|
|
||||||
"httparse",
|
|
||||||
"httpdate",
|
|
||||||
"itoa",
|
|
||||||
"pin-project-lite",
|
|
||||||
"socket2 0.5.9",
|
|
||||||
"tokio",
|
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
"want",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -1547,7 +1536,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"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",
|
||||||
"httparse",
|
"httparse",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -1564,7 +1553,7 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"hyper 1.6.0",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -1573,19 +1562,6 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper-tls"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"hyper 0.14.32",
|
|
||||||
"native-tls",
|
|
||||||
"tokio",
|
|
||||||
"tokio-native-tls",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -1594,7 +1570,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.6.0",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1613,8 +1589,8 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body",
|
||||||
"hyper 1.6.0",
|
"hyper",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -2006,9 +1982,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md5"
|
name = "md5"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
@@ -2034,9 +2010,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minidom"
|
name = "minidom"
|
||||||
version = "0.15.2"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278"
|
checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rxml",
|
"rxml",
|
||||||
]
|
]
|
||||||
@@ -2094,6 +2070,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2172,6 +2157,25 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-foundation"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-io-kit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
@@ -2309,7 +2313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2460,9 +2464,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.38.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
|
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2650,13 +2654,14 @@ dependencies = [
|
|||||||
"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",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.6.0",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"hyper-tls 0.6.0",
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
@@ -2671,12 +2676,14 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2761,9 +2768,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-s3"
|
name = "rust-s3"
|
||||||
version = "0.35.1"
|
version = "0.37.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3df3f353b1f4209dcf437d777cda90279c397ab15a0cd6fd06bd32c88591533"
|
checksum = "94f9b973bd4097f5bb47e5827dcb9fb5dc17e93879e46badc27d2a4e9a4e5588"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"aws-creds",
|
"aws-creds",
|
||||||
@@ -2771,27 +2778,25 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"http 0.2.12",
|
"http 1.3.1",
|
||||||
"hyper 0.14.32",
|
|
||||||
"hyper-tls 0.5.0",
|
|
||||||
"log",
|
"log",
|
||||||
"maybe-async",
|
"maybe-async",
|
||||||
"md5",
|
"md5",
|
||||||
"minidom",
|
"minidom",
|
||||||
"native-tls",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 1.0.69",
|
"sysinfo",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@@ -2865,20 +2870,22 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rxml"
|
name = "rxml"
|
||||||
version = "0.9.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7"
|
checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"rxml_validation",
|
"rxml_validation",
|
||||||
"smartstring",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rxml_validation"
|
name = "rxml_validation"
|
||||||
version = "0.9.1"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530"
|
checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4"
|
||||||
|
dependencies = [
|
||||||
|
"compact_str",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
@@ -3078,17 +3085,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smartstring"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"static_assertions",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
@@ -3178,7 +3174,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -3262,7 +3258,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
@@ -3301,7 +3297,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
@@ -3327,7 +3323,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3412,6 +3408,20 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysinfo"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"ntapi",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-io-kit",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -3446,33 +3456,13 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.12",
|
"thiserror-impl",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3651,7 +3641,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body",
|
||||||
"iri-string",
|
"iri-string",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -3984,6 +3974,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@@ -4054,6 +4057,28 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.61.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core",
|
||||||
|
"windows-future",
|
||||||
|
"windows-link 0.1.1",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -4067,6 +4092,17 @@ dependencies = [
|
|||||||
"windows-strings 0.4.2",
|
"windows-strings 0.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link 0.1.1",
|
||||||
|
"windows-threading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.0"
|
version = "0.60.0"
|
||||||
@@ -4101,6 +4137,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -4213,6 +4259,15 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.53.0",
|
"windows_x86_64_msvc 0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -4,37 +4,26 @@ 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"
|
||||||
readme = "../README.md"
|
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]
|
||||||
|
lib = { path = "../lib" }
|
||||||
actix-web = "4.11.0"
|
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.42", features = ["serde"] }
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
|
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
reqwest = "0.12.23"
|
|
||||||
serde = {version = "1.0.219", features = ["derive"]}
|
serde = {version = "1.0.219", features = ["derive"]}
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt", "time"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt", "time"] }
|
||||||
uuid = { version = "1.18.0", features = ["serde", "v4"] }
|
log = "0.4.28"
|
||||||
log = "0.4.27"
|
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
redis = { version = "0.32.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
|
||||||
regex = "1.11.1"
|
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
rust-s3 = "0.35.1"
|
|
||||||
rand = "0.9.2"
|
|
||||||
rand_chacha = "0.9.0"
|
|
||||||
futures = "0.3.31"
|
|
||||||
utoipa = { version = "5.4.0", 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"
|
|
||||||
lettre = { version = "0.11.18", 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.1"
|
|
||||||
flate2 = "1.1.2"
|
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
FROM rust:bookworm AS builder
|
FROM rust:bookworm AS builder
|
||||||
WORKDIR /builder
|
WORKDIR /builder
|
||||||
|
|
||||||
COPY api/migrations ./migrations
|
COPY crates/lib /lib
|
||||||
COPY api/src ./src
|
COPY crates/api/src ./src
|
||||||
COPY api/Cargo.toml ./
|
COPY crates/api/Cargo.toml ./
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y cmake
|
RUN apt-get update && apt-get install -y cmake
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
@@ -2,11 +2,11 @@ use std::future::Future;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use super::{SESSION_COOKIE_NAME, Session};
|
use super::{SESSION_COOKIE_NAME, Session};
|
||||||
use crate::account::user::User;
|
use crate::error::{ApiResult, Error};
|
||||||
use crate::error::Error;
|
|
||||||
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http, web};
|
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http, web};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::state::AppState;
|
use lib::accounts::User;
|
||||||
|
use lib::state::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Auth {
|
pub struct Auth {
|
||||||
@@ -104,3 +104,16 @@ impl FromRequest for Auth {
|
|||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
pub fn verify_role(&self, role: &str) -> ApiResult<()> {
|
||||||
|
if self.user.role == role {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error {
|
||||||
|
status: 403,
|
||||||
|
details: "User does not have permission to perform this action.".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
use crate::account::{csprng, hash};
|
use lib::accounts::{csprng, hash};
|
||||||
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 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 lib::state::AppState;
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct EmailToken {
|
pub struct EmailToken {
|
||||||
@@ -30,7 +29,9 @@ impl EmailToken {
|
|||||||
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();
|
||||||
state.set_ex(&key, &value, ttl as u64).await
|
let _ = state.set_ex(&key, &value, ttl as u64).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(state: &AppState, token: &str) -> ApiResult<Self> {
|
pub async fn get(state: &AppState, token: &str) -> ApiResult<Self> {
|
||||||
@@ -42,7 +43,8 @@ impl EmailToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(state: &AppState, token: &str) -> ApiResult<()> {
|
pub async fn delete(state: &AppState, token: &str) -> ApiResult<()> {
|
||||||
state.del(token).await
|
let _ = state.del(token).await;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
7
crates/api/src/accounts/mod.rs
Normal file
7
crates/api/src/accounts/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mod auth;
|
||||||
|
mod email_token;
|
||||||
|
mod session;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use email_token::*;
|
||||||
|
pub use session::*;
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
use super::{csprng, hash, verify_hash};
|
use lib::accounts::{csprng, hash, verify_hash};
|
||||||
use crate::error::{ApiResult, Error};
|
use crate::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::aio::ConnectionManager;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::task;
|
use lib::error::CoreResult;
|
||||||
use crate::state::AppState;
|
use lib::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";
|
||||||
@@ -42,7 +40,7 @@ impl Session {
|
|||||||
pub async fn store(&self, state: &AppState) -> ApiResult<()> {
|
pub async fn store(&self, state: &AppState) -> ApiResult<()> {
|
||||||
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: ApiResult<()> = match self.expires_at {
|
let result: CoreResult<()> = 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();
|
||||||
state.set_ex(&key, &value, ttl as u64).await
|
state.set_ex(&key, &value, ttl as u64).await
|
||||||
@@ -73,7 +71,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(state: &AppState, session_id: &str) -> ApiResult<()> {
|
pub async fn delete(state: &AppState, session_id: &str) -> ApiResult<()> {
|
||||||
let result: ApiResult<()> = state.del(session_id).await;
|
let result: CoreResult<()> = state.del(session_id).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
133
crates/api/src/error.rs
Normal file
133
crates/api/src/error.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use log::warn;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::fmt;
|
||||||
|
use lib::error::{CoreError, CoreErrorKind};
|
||||||
|
|
||||||
|
pub type ApiResult<T> = Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Error {
|
||||||
|
pub status: u16,
|
||||||
|
pub details: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn new(status: u16, details: String) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_http_response(&self) -> HttpResponse {
|
||||||
|
let status = StatusCode::from_u16(self.status).unwrap_or_else(|err| {
|
||||||
|
warn!("{}", err);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
});
|
||||||
|
HttpResponse::build(status).body(self.details.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str(self.details.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
&self.details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for Error {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
let status =
|
||||||
|
StatusCode::from_u16(self.status).unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
|
||||||
|
let status_code = status.as_u16();
|
||||||
|
let details = match status_code {
|
||||||
|
401 => String::from("Unauthorized"),
|
||||||
|
code if code < 500 => self.details.clone(),
|
||||||
|
_ => {
|
||||||
|
log::error!("Internal server error: {}", self.details);
|
||||||
|
String::from("Internal Server Error")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponse::build(status).json(json!({ "status": status_code, "details": details }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
Self::new(500, format!("Unknown IO error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::env::VarError> for Error {
|
||||||
|
fn from(error: std::env::VarError) -> Self {
|
||||||
|
Self::new(
|
||||||
|
500,
|
||||||
|
format!("Unknown environment variable error: {:?}", error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Self::new(500, format!("Unknown serde_json error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<argon2::password_hash::Error> for Error {
|
||||||
|
fn from(error: argon2::password_hash::Error) -> Self {
|
||||||
|
Self::new(500, format!("Unknown argon2 error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lettre::address::AddressError> for Error {
|
||||||
|
fn from(error: lettre::address::AddressError) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lettre::error::Error> for Error {
|
||||||
|
fn from(error: lettre::error::Error) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lettre::transport::smtp::Error> for Error {
|
||||||
|
fn from(error: lettre::transport::smtp::Error) -> Self {
|
||||||
|
Error::new(500, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Error {
|
||||||
|
fn from(error: String) -> Self {
|
||||||
|
Self::new(500, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CoreError> for Error {
|
||||||
|
fn from(error: CoreError) -> Self {
|
||||||
|
match error.kind {
|
||||||
|
CoreErrorKind::NotFound => Self::new(404, error.to_string()),
|
||||||
|
CoreErrorKind::InvalidInput => Self::new(400, error.to_string()),
|
||||||
|
CoreErrorKind::Conflict => Self::new(409, error.to_string()),
|
||||||
|
CoreErrorKind::Unauthorized => Self::new(401, error.to_string()),
|
||||||
|
CoreErrorKind::Forbidden => Self::new(403, error.to_string()),
|
||||||
|
CoreErrorKind::PreconditionFailed => Self::new(412, error.to_string()),
|
||||||
|
CoreErrorKind::Timeout => Self::new(408, error.to_string()),
|
||||||
|
CoreErrorKind::Cancelled => Self::new(499, error.to_string()),
|
||||||
|
CoreErrorKind::Unavailable => Self::new(503, error.to_string()),
|
||||||
|
CoreErrorKind::Internal => Self::new(500, error.to_string()),
|
||||||
|
CoreErrorKind::External => Self::new(502, error.to_string()),
|
||||||
|
_ => Self::new(500, error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,26 @@
|
|||||||
use crate::account::User;
|
use lib::accounts::{User, ADMIN_ROLE, hash};
|
||||||
use crate::account::{ADMIN_ROLE, hash};
|
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||||
use dotenv::from_filename;
|
use dotenv::from_filename;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::Arc;
|
|
||||||
use utoipa::openapi::SecurityRequirement;
|
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;
|
use lib::state::AppState;
|
||||||
|
|
||||||
mod account;
|
mod accounts;
|
||||||
mod airports;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod http_client;
|
mod routes;
|
||||||
mod metars;
|
|
||||||
mod scheduler;
|
|
||||||
mod smtp;
|
mod smtp;
|
||||||
mod system;
|
mod system;
|
||||||
mod state;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[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()?;
|
||||||
let state = Arc::new(AppState::new().await?);
|
|
||||||
scheduler::run(state.clone());
|
let state = AppState::new().await?;
|
||||||
|
|
||||||
// Initialize admin user
|
// Initialize admin user
|
||||||
let admin_username = env::var("ADMIN_USERNAME");
|
let admin_username = env::var("ADMIN_USERNAME");
|
||||||
@@ -82,9 +76,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.into_utoipa_app()
|
.into_utoipa_app()
|
||||||
.service(
|
.service(
|
||||||
scope::scope("/api")
|
scope::scope("/api")
|
||||||
.configure(airports::init_routes)
|
.configure(routes::init_routes)
|
||||||
.configure(metars::init_routes)
|
|
||||||
.configure(account::init_routes)
|
|
||||||
.configure(system::init_routes),
|
.configure(system::init_routes),
|
||||||
)
|
)
|
||||||
.split_for_parts();
|
.split_for_parts();
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
use crate::{
|
use lib::accounts::{csprng, LoginRequest, RegisterRequest, UpdateUser, User, UserResponse, UserFavorite, verify_hash};
|
||||||
account::{SESSION_COOKIE_NAME, Session, verify_hash},
|
|
||||||
error::Error,
|
|
||||||
};
|
|
||||||
use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web};
|
use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
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 lib::error::CoreErrorKind;
|
||||||
use crate::account::email_token::{EmailToken, send_confirm_email, send_password_reset_email};
|
use lib::state::AppState;
|
||||||
use crate::account::user::{LoginRequest, RegisterRequest, UpdateUser, User, UserResponse};
|
use crate::accounts::{send_confirm_email, send_password_reset_email, Auth, EmailToken, Session, SESSION_COOKIE_NAME};
|
||||||
use crate::account::user_favorites::UserFavorite;
|
use crate::error::Error;
|
||||||
use crate::account::{Auth, csprng};
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
tag = "account",
|
tag = "account",
|
||||||
@@ -30,12 +25,12 @@ async fn register(state: web::Data<AppState>, user: web::Json<RegisterRequest>,
|
|||||||
let username = register_user.username.clone();
|
let username = register_user.username.clone();
|
||||||
let email = register_user.email.clone();
|
let email = register_user.email.clone();
|
||||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||||
let insert_user: User = match register_user.to_user() {
|
let insert_user: User = match register_user.to_user().map_err(Error::from) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(&err),
|
||||||
};
|
};
|
||||||
|
|
||||||
match insert_user.insert(&state.pool).await {
|
match insert_user.insert(&state.pool).await.map_err(Error::from) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let user_response: UserResponse = user.into();
|
let user_response: UserResponse = user.into();
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -123,7 +118,7 @@ async fn confirm_email_registration(
|
|||||||
avatar: None,
|
avatar: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match update_user.update(&state.pool, &user.username).await {
|
match update_user.update(&state.pool, &user.username).await.map_err(Error::from) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let response: UserResponse = user.into();
|
let response: UserResponse = user.into();
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -451,7 +446,7 @@ async fn change_password(
|
|||||||
avatar: None,
|
avatar: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match update_user.update(&state.pool, &username).await {
|
match update_user.update(&state.pool, &username).await.map_err(Error::from) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let response: UserResponse = user.into();
|
let response: UserResponse = user.into();
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -532,10 +527,10 @@ async fn confirm_password_reset(
|
|||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
// TODO
|
// TODO
|
||||||
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(&state, token).await {
|
let _email_token = match EmailToken::get(&state, token).await {
|
||||||
Ok(password_reset) => {
|
Ok(password_reset) => {
|
||||||
if let Err(err) = EmailToken::delete(&state, &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);
|
||||||
@@ -563,7 +558,7 @@ async fn confirm_password_reset(
|
|||||||
#[get("/profile/favorites")]
|
#[get("/profile/favorites")]
|
||||||
async fn get_favorites(state: web::Data<AppState>, 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(&state.pool, &username).await {
|
match UserFavorite::select_all(&state.pool, &username).await.map_err(Error::from) {
|
||||||
Ok(favorites) => HttpResponse::Ok().json(favorites),
|
Ok(favorites) => HttpResponse::Ok().json(favorites),
|
||||||
Err(err) => ResponseError::error_response(&err),
|
Err(err) => ResponseError::error_response(&err),
|
||||||
}
|
}
|
||||||
@@ -582,7 +577,7 @@ async fn get_favorites(state: web::Data<AppState>, auth: Auth) -> HttpResponse {
|
|||||||
#[post("/profile/favorites/{icao}")]
|
#[post("/profile/favorites/{icao}")]
|
||||||
async fn add_favorite(state: web::Data<AppState>, 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(&state.pool, &username, &icao.into_inner()).await {
|
match UserFavorite::insert(&state.pool, &username, &icao.into_inner()).await.map_err(Error::from) {
|
||||||
Ok(_) => HttpResponse::Ok().finish(),
|
Ok(_) => HttpResponse::Ok().finish(),
|
||||||
Err(err) => ResponseError::error_response(&err),
|
Err(err) => ResponseError::error_response(&err),
|
||||||
}
|
}
|
||||||
@@ -601,7 +596,7 @@ async fn add_favorite(state: web::Data<AppState>, icao: web::Path<String>, auth:
|
|||||||
#[delete("/profile/favorites/{icao}")]
|
#[delete("/profile/favorites/{icao}")]
|
||||||
async fn remove_favorite(state: web::Data<AppState>, 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(&state.pool, &username, &icao.into_inner()).await {
|
match UserFavorite::delete(&state.pool, &username, &icao.into_inner()).await.map_err(Error::from) {
|
||||||
Ok(_) => HttpResponse::Ok().finish(),
|
Ok(_) => HttpResponse::Ok().finish(),
|
||||||
Err(err) => ResponseError::error_response(&err),
|
Err(err) => ResponseError::error_response(&err),
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
use futures_util::stream::StreamExt as _;
|
use futures_util::stream::StreamExt as _;
|
||||||
|
|
||||||
use crate::account::ADMIN_ROLE;
|
use lib::{accounts::ADMIN_ROLE, airports::{Airport, AirportQuery, UpdateAirport}};
|
||||||
use crate::airports::{AirportQuery, UpdateAirport};
|
|
||||||
use crate::{
|
|
||||||
account::{Auth, verify_role},
|
|
||||||
airports::Airport,
|
|
||||||
};
|
|
||||||
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 lib::state::AppState;
|
||||||
|
use crate::accounts::Auth;
|
||||||
|
use crate::error::Error;
|
||||||
use crate::utils::Paged;
|
use crate::utils::Paged;
|
||||||
|
|
||||||
#[derive(ToSchema)]
|
#[derive(ToSchema)]
|
||||||
@@ -36,8 +33,8 @@ struct FileUpload {
|
|||||||
)]
|
)]
|
||||||
#[post("/import")]
|
#[post("/import")]
|
||||||
async fn import_airports(state: web::Data<AppState>, 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) = &auth.verify_role(ADMIN_ROLE).map_err(Error::from) {
|
||||||
return ResponseError::error_response(&err);
|
return ResponseError::error_response(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
@@ -68,7 +65,7 @@ async fn import_airports(state: web::Data<AppState>, mut payload: Multipart, aut
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match Airport::insert_all(&state.pool, airports).await {
|
match Airport::insert_all(&state.pool, airports).await.map_err(Error::from) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(&err),
|
||||||
};
|
};
|
||||||
@@ -104,7 +101,7 @@ async fn get_airports(state: web::Data<AppState>, req: HttpRequest) -> HttpRespo
|
|||||||
query.limit = Some(limit);
|
query.limit = Some(limit);
|
||||||
query.page = Some(page);
|
query.page = Some(page);
|
||||||
|
|
||||||
match Airport::select_all(&state.pool, &query).await {
|
match Airport::select_all(&state.pool, &query).await.map_err(Error::from) {
|
||||||
Ok(airports) => HttpResponse::Ok().json(Paged {
|
Ok(airports) => HttpResponse::Ok().json(Paged {
|
||||||
data: airports,
|
data: airports,
|
||||||
page,
|
page,
|
||||||
@@ -154,11 +151,11 @@ async fn get_airport(state: web::Data<AppState>, icao: web::Path<String>, req: H
|
|||||||
)]
|
)]
|
||||||
#[post("")]
|
#[post("")]
|
||||||
async fn insert_airport(state: web::Data<AppState>, 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 &auth.verify_role(ADMIN_ROLE) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(err),
|
||||||
};
|
};
|
||||||
match airport.insert(&state.pool).await {
|
match airport.insert(&state.pool).await.map_err(Error::from) {
|
||||||
Ok(a) => HttpResponse::Ok().json(a),
|
Ok(a) => HttpResponse::Ok().json(a),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
@@ -184,11 +181,11 @@ async fn update_airport(
|
|||||||
airport: web::Json<UpdateAirport>,
|
airport: web::Json<UpdateAirport>,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
let _ = match &auth.verify_role(ADMIN_ROLE) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(err),
|
||||||
};
|
};
|
||||||
match Airport::update(&state.pool, &icao.into_inner(), &airport.into_inner()).await {
|
match Airport::update(&state.pool, &icao.into_inner(), &airport.into_inner()).await.map_err(Error::from) {
|
||||||
Ok(a) => HttpResponse::Ok().json(a),
|
Ok(a) => HttpResponse::Ok().json(a),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
@@ -209,11 +206,11 @@ async fn update_airport(
|
|||||||
)]
|
)]
|
||||||
#[delete("")]
|
#[delete("")]
|
||||||
async fn delete_airports(state: web::Data<AppState>, auth: Auth) -> HttpResponse {
|
async fn delete_airports(state: web::Data<AppState>, auth: Auth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
let _ = match &auth.verify_role(ADMIN_ROLE) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(err),
|
||||||
};
|
};
|
||||||
match Airport::delete_all(&state.pool).await {
|
match Airport::delete_all(&state.pool).await.map_err(Error::from) {
|
||||||
Ok(_) => HttpResponse::NoContent().finish(),
|
Ok(_) => HttpResponse::NoContent().finish(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
@@ -234,11 +231,11 @@ async fn delete_airports(state: web::Data<AppState>, auth: Auth) -> HttpResponse
|
|||||||
)]
|
)]
|
||||||
#[delete("/{icao}")]
|
#[delete("/{icao}")]
|
||||||
async fn delete_airport(state: web::Data<AppState>, 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 &auth.verify_role(ADMIN_ROLE) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => return ResponseError::error_response(&err),
|
Err(err) => return ResponseError::error_response(err),
|
||||||
};
|
};
|
||||||
match Airport::delete(&state.pool, &icao.into_inner()).await {
|
match Airport::delete(&state.pool, &icao.into_inner()).await.map_err(Error::from) {
|
||||||
Ok(_) => HttpResponse::NoContent().finish(),
|
Ok(_) => HttpResponse::NoContent().finish(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::account::Auth;
|
|
||||||
use crate::metars::Metar;
|
|
||||||
use actix_web::{HttpRequest, HttpResponse, get, put, web};
|
use actix_web::{HttpRequest, HttpResponse, get, put, web};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, 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 lib::metars::Metar;
|
||||||
|
use crate::accounts::Auth;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
#[into_params(parameter_in = Query)]
|
#[into_params(parameter_in = Query)]
|
||||||
@@ -37,7 +38,7 @@ async fn find_all(state: web::Data<AppState>, 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(&state.pool, &icaos).await {
|
let metars = match Metar::get_all_distinct(&state.pool, &icaos).await.map_err(Error::from) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
@@ -62,7 +63,6 @@ async fn find_all(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse
|
|||||||
)]
|
)]
|
||||||
#[put("")]
|
#[put("")]
|
||||||
async fn refresh_metars(state: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse {
|
async fn refresh_metars(state: web::Data<AppState>, req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||||
let client = state.client.clone();
|
|
||||||
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
||||||
let icao_option = ¶meters.icaos;
|
let icao_option = ¶meters.icaos;
|
||||||
if let None = icao_option {
|
if let None = icao_option {
|
||||||
@@ -75,7 +75,7 @@ async fn refresh_metars(state: web::Data<AppState>, req: HttpRequest, _auth: Aut
|
|||||||
};
|
};
|
||||||
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(&state, &icaos).await {
|
let metars = match Metar::get_or_update_metars(&state, &icaos).await.map_err(Error::from) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
12
crates/api/src/routes/mod.rs
Normal file
12
crates/api/src/routes/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use utoipa_actix_web::service_config::ServiceConfig;
|
||||||
|
|
||||||
|
mod accounts;
|
||||||
|
mod airports;
|
||||||
|
mod metars;
|
||||||
|
|
||||||
|
pub fn init_routes(config: &mut ServiceConfig) {
|
||||||
|
config
|
||||||
|
.configure(accounts::init_routes)
|
||||||
|
.configure(airports::init_routes)
|
||||||
|
.configure(metars::init_routes);
|
||||||
|
}
|
||||||
24
crates/lib/Cargo.toml
Normal file
24
crates/lib/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "lib"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
chrono = { version = "0.4.42", features = ["serde"] }
|
||||||
|
log = "0.4.28"
|
||||||
|
rand = "0.9.2"
|
||||||
|
rand_chacha = "0.9.0"
|
||||||
|
serde = { version = "1.0.226", features = ["derive"] }
|
||||||
|
serde_json = "1.0.142"
|
||||||
|
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
|
||||||
|
utoipa = { version = "5.4.0", features = ["chrono", "uuid", "actix_extras"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["serde", "v4"] }
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
flate2 = "1.1.2"
|
||||||
|
reqwest = "0.12.23"
|
||||||
|
regex = "1.11.2"
|
||||||
|
redis = { version = "0.32.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
|
||||||
|
governor = "0.10.1"
|
||||||
|
tokio = { version = "1.47.1", features = ["macros", "rt", "time"] }
|
||||||
|
rust-s3 = "0.37.0"
|
||||||
9
crates/lib/src/accounts/mod.rs
Normal file
9
crates/lib/src/accounts/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod password_requirements;
|
||||||
|
mod user;
|
||||||
|
mod user_favorites;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use password_requirements::*;
|
||||||
|
pub use user::*;
|
||||||
|
pub use user_favorites::*;
|
||||||
|
pub use utils::*;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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::{Pool, Postgres, QueryBuilder};
|
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
use crate::accounts::hash;
|
||||||
|
use crate::error::CoreResult;
|
||||||
|
|
||||||
pub const ADMIN_ROLE: &str = "ADMIN";
|
pub const ADMIN_ROLE: &str = "ADMIN";
|
||||||
pub const USER_ROLE: &str = "USER";
|
pub const USER_ROLE: &str = "USER";
|
||||||
@@ -33,7 +34,7 @@ pub struct RegisterRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterRequest {
|
impl RegisterRequest {
|
||||||
pub fn to_user(self) -> ApiResult<User> {
|
pub fn to_user(self) -> CoreResult<User> {
|
||||||
let password_hash = hash(&self.password)?;
|
let password_hash = hash(&self.password)?;
|
||||||
Ok(User {
|
Ok(User {
|
||||||
username: self.username,
|
username: self.username,
|
||||||
@@ -106,7 +107,7 @@ pub struct UpdateUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateUser {
|
impl UpdateUser {
|
||||||
pub async fn update(&self, pool: &Pool<Postgres>, username: &str) -> ApiResult<User> {
|
pub async fn update(&self, pool: &Pool<Postgres>, username: &str) -> CoreResult<User> {
|
||||||
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));
|
||||||
|
|
||||||
@@ -235,7 +236,7 @@ impl User {
|
|||||||
.unwrap_or_else(|_| 0)
|
.unwrap_or_else(|_| 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<User> {
|
pub async fn insert(&self, pool: &Pool<Postgres>) -> CoreResult<User> {
|
||||||
let user: User = sqlx::query_as::<_, Self>(&format!(
|
let user: User = sqlx::query_as::<_, Self>(&format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO {} (
|
INSERT INTO {} (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::error::ApiResult;
|
use crate::error::CoreResult;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ pub struct UserFavorite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserFavorite {
|
impl UserFavorite {
|
||||||
pub async fn select_all(pool: &Pool<Postgres>, username: &str) -> ApiResult<Vec<String>> {
|
pub async fn select_all(pool: &Pool<Postgres>, username: &str) -> CoreResult<Vec<String>> {
|
||||||
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
|
||||||
@@ -27,7 +27,7 @@ impl UserFavorite {
|
|||||||
Ok(favorites)
|
Ok(favorites)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
|
pub async fn insert(pool: &Pool<Postgres>, username: &str, icao: &str) -> CoreResult<()> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO {} (
|
INSERT INTO {} (
|
||||||
@@ -44,7 +44,7 @@ impl UserFavorite {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(pool: &Pool<Postgres>, username: &str, icao: &str) -> ApiResult<()> {
|
pub async fn delete(pool: &Pool<Postgres>, username: &str, icao: &str) -> CoreResult<()> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM {} WHERE username = $1 AND icao = $2
|
DELETE FROM {} WHERE username = $1 AND icao = $2
|
||||||
@@ -1,25 +1,11 @@
|
|||||||
use argon2::{
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
password_hash::{SaltString, rand_core::OsRng},
|
use argon2::password_hash::SaltString;
|
||||||
};
|
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rand::prelude::*;
|
use rand::Rng;
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
use rand_chacha::rand_core::SeedableRng;
|
||||||
mod auth;
|
use crate::error::CoreResult;
|
||||||
mod email_token;
|
|
||||||
mod model;
|
|
||||||
mod routes;
|
|
||||||
mod session;
|
|
||||||
mod user;
|
|
||||||
mod user_favorites;
|
|
||||||
|
|
||||||
pub use auth::*;
|
|
||||||
pub use routes::init_routes;
|
|
||||||
pub use session::*;
|
|
||||||
pub use user::*;
|
|
||||||
|
|
||||||
use crate::error::{ApiResult, Error};
|
|
||||||
|
|
||||||
pub fn csprng(take: usize) -> String {
|
pub fn csprng(take: usize) -> String {
|
||||||
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
||||||
@@ -31,7 +17,7 @@ pub fn csprng(take: usize) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash(string: &str) -> ApiResult<String> {
|
pub fn hash(string: &str) -> CoreResult<String> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
let hash = Argon2::default()
|
let hash = Argon2::default()
|
||||||
.hash_password(string.as_bytes(), &salt)?
|
.hash_password(string.as_bytes(), &salt)?
|
||||||
@@ -57,17 +43,6 @@ pub fn verify_hash(string: &str, hashed_string: &str) -> bool {
|
|||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_role(auth: &Auth, role: &str) -> ApiResult<()> {
|
|
||||||
if auth.user.role == role {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error {
|
|
||||||
status: 403,
|
|
||||||
details: "User does not have permission to perform this action.".to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2,8 +2,6 @@ use crate::airports::{
|
|||||||
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
|
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
|
||||||
UpdateRunway,
|
UpdateRunway,
|
||||||
};
|
};
|
||||||
use crate::error::{ApiResult, Error};
|
|
||||||
use crate::metars::Metar;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures_util::try_join;
|
use futures_util::try_join;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -11,7 +9,8 @@ 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;
|
use crate::error::{CoreError, CoreErrorKind, CoreResult};
|
||||||
|
use crate::metars::Metar;
|
||||||
|
|
||||||
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, \
|
||||||
@@ -137,11 +136,11 @@ pub struct Bounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Bounds {
|
impl Bounds {
|
||||||
fn parse(input: &str) -> ApiResult<Bounds> {
|
fn parse(input: &str) -> CoreResult<Bounds> {
|
||||||
let parts: Vec<&str> = input.split(',').collect();
|
let parts: Vec<&str> = input.split(',').collect();
|
||||||
if parts.len() != 4 {
|
if parts.len() != 4 {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
400,
|
CoreErrorKind::InvalidInput,
|
||||||
format!("Expected 4 fields in bounds but received {}", parts.len()),
|
format!("Expected 4 fields in bounds but received {}", parts.len()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -332,7 +331,7 @@ impl Airport {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all(pool: &Pool<Postgres>, query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
pub async fn select_all(pool: &Pool<Postgres>, query: &AirportQuery) -> CoreResult<Vec<Self>> {
|
||||||
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));
|
||||||
|
|
||||||
@@ -487,7 +486,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, pool: &Pool<Postgres>) -> ApiResult<Self> {
|
pub async fn insert(&self, pool: &Pool<Postgres>) -> CoreResult<Self> {
|
||||||
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 {
|
||||||
@@ -535,7 +534,7 @@ impl Airport {
|
|||||||
Ok(airport.into())
|
Ok(airport.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_all(pool: &Pool<Postgres>, airports: Vec<Self>) -> ApiResult<()> {
|
pub async fn insert_all(pool: &Pool<Postgres>, airports: Vec<Self>) -> CoreResult<()> {
|
||||||
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();
|
||||||
@@ -592,7 +591,7 @@ impl Airport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
pub async fn update(pool: &Pool<Postgres>, icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
|
pub async fn update(pool: &Pool<Postgres>, icao: &str, airport: &UpdateAirport) -> CoreResult<()> {
|
||||||
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 {
|
||||||
@@ -607,7 +606,7 @@ impl Airport {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(pool: &Pool<Postgres>, icao: &str) -> ApiResult<()> {
|
pub async fn delete(pool: &Pool<Postgres>, icao: &str) -> CoreResult<()> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM {} WHERE icao = $1
|
DELETE FROM {} WHERE icao = $1
|
||||||
@@ -621,7 +620,7 @@ impl Airport {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all(pool: &Pool<Postgres>) -> ApiResult<()> {
|
pub async fn delete_all(pool: &Pool<Postgres>) -> CoreResult<()> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM {} WHERE true
|
DELETE FROM {} WHERE true
|
||||||
@@ -688,7 +687,7 @@ impl Airport {
|
|||||||
builder: &mut QueryBuilder<'a, Postgres>,
|
builder: &mut QueryBuilder<'a, Postgres>,
|
||||||
has_where: &mut bool,
|
has_where: &mut bool,
|
||||||
field: &'a Option<String>,
|
field: &'a Option<String>,
|
||||||
) -> ApiResult<()> {
|
) -> CoreResult<()> {
|
||||||
// Query bounds
|
// Query bounds
|
||||||
if let Some(bounds_string) = field {
|
if let Some(bounds_string) = field {
|
||||||
if !*has_where {
|
if !*has_where {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::error::ApiResult;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Pool, 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;
|
||||||
|
use crate::error::CoreResult;
|
||||||
|
|
||||||
const TABLE_NAME: &str = "communications";
|
const TABLE_NAME: &str = "communications";
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ impl Communication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> CoreResult<HashMap<String, Vec<Self>>> {
|
||||||
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
|
||||||
@@ -86,7 +86,7 @@ impl Communication {
|
|||||||
Ok(frequency_map)
|
Ok(frequency_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
|
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> CoreResult<Vec<Self>> {
|
||||||
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
|
||||||
@@ -99,7 +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(pool: &Pool<Postgres>, communications: &Vec<CommunicationRow>) -> ApiResult<()> {
|
pub async fn insert_all(pool: &Pool<Postgres>, communications: &Vec<CommunicationRow>) -> CoreResult<()> {
|
||||||
let chunk_size = 1000;
|
let chunk_size = 1000;
|
||||||
|
|
||||||
for chunk in communications.chunks(chunk_size) {
|
for chunk in communications.chunks(chunk_size) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::error::ApiResult;
|
use crate::error::CoreResult;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -63,7 +63,7 @@ impl Runway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
|
pub async fn select_all_map(pool: &Pool<Postgres>, icaos: &Vec<String>) -> CoreResult<HashMap<String, Vec<Self>>> {
|
||||||
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
|
||||||
@@ -82,7 +82,7 @@ impl Runway {
|
|||||||
Ok(runway_map)
|
Ok(runway_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> ApiResult<Vec<Self>> {
|
pub async fn select_all(pool: &Pool<Postgres>, icao: &str) -> CoreResult<Vec<Self>> {
|
||||||
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
|
||||||
@@ -95,7 +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(pool: &Pool<Postgres>, runways: &Vec<RunwayRow>) -> ApiResult<()> {
|
pub async fn insert_all(pool: &Pool<Postgres>, runways: &Vec<RunwayRow>) -> CoreResult<()> {
|
||||||
let chunk_size = 1000;
|
let chunk_size = 1000;
|
||||||
|
|
||||||
for chunk in runways.chunks(chunk_size) {
|
for chunk in runways.chunks(chunk_size) {
|
||||||
220
crates/lib/src/error.rs
Normal file
220
crates/lib/src/error.rs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::sync::{MutexGuard, PoisonError};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::de::StdError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum CoreErrorKind {
|
||||||
|
NotFound,
|
||||||
|
InvalidInput,
|
||||||
|
Conflict,
|
||||||
|
Unauthorized,
|
||||||
|
Forbidden,
|
||||||
|
PreconditionFailed,
|
||||||
|
Timeout,
|
||||||
|
Cancelled,
|
||||||
|
Unavailable,
|
||||||
|
Internal,
|
||||||
|
External,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CoreError {
|
||||||
|
pub kind: CoreErrorKind,
|
||||||
|
pub message: String,
|
||||||
|
pub context: Vec<(&'static str, String)>,
|
||||||
|
source: Option<Box<dyn StdError>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreError {
|
||||||
|
pub fn new(kind: CoreErrorKind, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
message: message.into(),
|
||||||
|
context: vec![],
|
||||||
|
source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_source(kind: CoreErrorKind, message: impl Into<String>, source: impl StdError + Send + Sync + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
message: message.into(),
|
||||||
|
context: vec![],
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(mut self, context: Vec<(&'static str, String)>) -> Self {
|
||||||
|
self.context = context;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CoreError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{:?} - {}", self.kind, self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for CoreError {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
self.source.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CoreResult<T> = Result<T, CoreError>;
|
||||||
|
|
||||||
|
pub fn not_found(entity: &'static str, id: impl Into<String>) -> CoreError {
|
||||||
|
CoreError::new(CoreErrorKind::NotFound, format!("{entity} not found: {}", id.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl From<argon2::password_hash::Error> for CoreError {
|
||||||
|
fn from(error: argon2::password_hash::Error) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for CoreError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown IO error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<redis::RedisError> for CoreError {
|
||||||
|
fn from(error: redis::RedisError) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown redis error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for CoreError {
|
||||||
|
fn from(error: sqlx::Error) -> Self {
|
||||||
|
match error {
|
||||||
|
sqlx::Error::RowNotFound => CoreError::new(CoreErrorKind::NotFound, "Not found".to_string()),
|
||||||
|
sqlx::Error::ColumnIndexOutOfBounds { .. } => CoreError::new(CoreErrorKind::InvalidInput, error.to_string()),
|
||||||
|
sqlx::Error::ColumnNotFound { .. } => CoreError::new(CoreErrorKind::NotFound, error.to_string()),
|
||||||
|
sqlx::Error::ColumnDecode { .. } => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Decode(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::PoolTimedOut => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::PoolClosed => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Tls(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Io(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Protocol(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Configuration(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::AnyDriverError(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::Database(err) => {
|
||||||
|
if let Some(code) = err.code() {
|
||||||
|
match code.trim() {
|
||||||
|
// Unique violation
|
||||||
|
"23505" => return CoreError::new(CoreErrorKind::External, err.to_string()),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CoreError::new(CoreErrorKind::External, err.to_string())
|
||||||
|
}
|
||||||
|
sqlx::Error::Migrate(_) => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
sqlx::Error::TypeNotFound { type_name } => {
|
||||||
|
CoreError::new(CoreErrorKind::External, format!("Type not found: {}", type_name))
|
||||||
|
}
|
||||||
|
sqlx::Error::WorkerCrashed => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
_ => CoreError::new(CoreErrorKind::External, error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for CoreError {
|
||||||
|
fn from(error: reqwest::Error) -> Self {
|
||||||
|
match error.status() {
|
||||||
|
Some(status_code) => {
|
||||||
|
if status_code.is_client_error() {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Client reqwest error: {:?}", error))
|
||||||
|
} else if status_code.is_server_error() {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Server reqwest error: {:?}", error))
|
||||||
|
} else {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown reqwest error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Self::new(CoreErrorKind::External, format!("Unknown reqwest error: {:?}", error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<s3::error::S3Error> for CoreError {
|
||||||
|
fn from(error: s3::error::S3Error) -> Self {
|
||||||
|
match error {
|
||||||
|
s3::error::S3Error::Credentials(err) => {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown s3 credentials error: {:?}", err))
|
||||||
|
}
|
||||||
|
s3::error::S3Error::FromUtf8(err) => {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown s3 from utf8 error: {:?}", err))
|
||||||
|
}
|
||||||
|
s3::error::S3Error::FmtError(err) => {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown s3 fmt error: {:?}", err))
|
||||||
|
}
|
||||||
|
s3::error::S3Error::HmacInvalidLength(err) => Self::new(
|
||||||
|
CoreErrorKind::External,
|
||||||
|
format!("Unknown s3 hmac invalid length error: {:?}", err),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
let re = Regex::new(r"HTTP (\d{3})").unwrap();
|
||||||
|
// Apply the regex to the input string
|
||||||
|
if let Some(captures) = re.captures(&error.to_string()) {
|
||||||
|
if let Some(http_code_str) = captures.get(1) {
|
||||||
|
if let Ok(http_code) = http_code_str.as_str().parse::<u16>() {
|
||||||
|
return Self::new(CoreErrorKind::External, error.to_string()).context(vec![("http_code", http_code.to_string())]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown s3 error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::env::VarError> for CoreError {
|
||||||
|
fn from(error: std::env::VarError) -> Self {
|
||||||
|
Self::new(
|
||||||
|
CoreErrorKind::External,
|
||||||
|
format!("Unknown environment variable error: {:?}", error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for CoreError {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Unknown serde_json error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> From<PoisonError<MutexGuard<'a, T>>> for CoreError {
|
||||||
|
fn from(_: PoisonError<MutexGuard<'a, T>>) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, "Failed to acquire lock".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<core::num::ParseIntError> for CoreError {
|
||||||
|
fn from(error: core::num::ParseIntError) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Integer parse error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<core::num::ParseFloatError> for CoreError {
|
||||||
|
fn from(error: core::num::ParseFloatError) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Float parse error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<regex::Error> for CoreError {
|
||||||
|
fn from(error: regex::Error) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chrono::ParseError> for CoreError {
|
||||||
|
fn from(error: chrono::ParseError) -> Self {
|
||||||
|
Self::new(CoreErrorKind::External, format!("Chrono parse error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::error::{ApiResult, Error};
|
use crate::error::{CoreResult, CoreError, CoreErrorKind};
|
||||||
use governor::clock::DefaultClock;
|
use governor::clock::DefaultClock;
|
||||||
use governor::state::{InMemoryState, NotKeyed};
|
use governor::state::{InMemoryState, NotKeyed};
|
||||||
use governor::{Quota, RateLimiter};
|
use governor::{Quota, RateLimiter};
|
||||||
@@ -18,7 +18,7 @@ pub struct HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
pub fn new(default_retry_after: u64) -> ApiResult<Self> {
|
pub fn new(default_retry_after: u64) -> CoreResult<Self> {
|
||||||
let mut client_builder = Client::builder()
|
let mut client_builder = Client::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
.tls_built_in_root_certs(true);
|
.tls_built_in_root_certs(true);
|
||||||
@@ -45,11 +45,11 @@ impl HttpClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default() -> ApiResult<Self> {
|
pub fn default() -> CoreResult<Self> {
|
||||||
Self::new(60)
|
Self::new(60)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&self, url: &str, etag: Option<String>) -> ApiResult<Response> {
|
pub async fn get(&self, url: &str, etag: Option<String>) -> CoreResult<Response> {
|
||||||
self.limiter.until_ready().await;
|
self.limiter.until_ready().await;
|
||||||
|
|
||||||
let mut request = self.client.get(url);
|
let mut request = self.client.get(url);
|
||||||
@@ -81,8 +81,8 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
response.status().as_u16(),
|
CoreErrorKind::External,
|
||||||
format!("Request returned status {}", response.status()),
|
format!("Request returned status {}", response.status()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
6
crates/lib/src/lib.rs
Normal file
6
crates/lib/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod accounts;
|
||||||
|
pub mod airports;
|
||||||
|
pub mod metars;
|
||||||
|
pub mod http_client;
|
||||||
|
pub mod state;
|
||||||
|
pub mod error;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::error::ApiResult;
|
|
||||||
use crate::metars::Metar;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::error::CoreResult;
|
||||||
|
use crate::metars::model::Metar;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -31,7 +31,7 @@ impl MetarCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(state: &AppState, icao: &str) -> Option<MetarCheck> {
|
pub async fn get(state: &AppState, icao: &str) -> Option<MetarCheck> {
|
||||||
let result: ApiResult<Option<String>> = state.get(icao).await;
|
let result: CoreResult<Option<String>> = state.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),
|
||||||
@@ -48,7 +48,7 @@ impl MetarCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert(&self, state: &AppState) -> ApiResult<()> {
|
pub async fn insert(&self, state: &AppState) -> CoreResult<()> {
|
||||||
let value = serde_json::to_string(&self)?;
|
let value = serde_json::to_string(&self)?;
|
||||||
state.set(self.icao.as_str(), &value).await?;
|
state.set(self.icao.as_str(), &value).await?;
|
||||||
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
mod metar_check;
|
mod metar_check;
|
||||||
mod model;
|
mod model;
|
||||||
mod routes;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use metar_check::*;
|
pub use metar_check::*;
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
pub use routes::init_routes;
|
pub use utils::*;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::airports::{Airport, UpdateAirport};
|
use crate::airports::{Airport, UpdateAirport};
|
||||||
use crate::error::Error;
|
use crate::error::{CoreError, CoreErrorKind, CoreResult};
|
||||||
use crate::metars::MetarCheck;
|
|
||||||
use crate::metars::utils::parse_metar_time;
|
use crate::metars::utils::parse_metar_time;
|
||||||
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;
|
||||||
@@ -16,6 +14,7 @@ use std::str::FromStr;
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
use crate::metars::metar_check::MetarCheck;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
|
static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
|
||||||
@@ -87,12 +86,12 @@ pub enum ReportModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ReportModifier {
|
impl FromStr for ReportModifier {
|
||||||
type Err = Error;
|
type Err = CoreError;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"AUTO" => Ok(ReportModifier::Auto),
|
"AUTO" => Ok(ReportModifier::Auto),
|
||||||
"COR" => Ok(ReportModifier::Corrected),
|
"COR" => Ok(ReportModifier::Corrected),
|
||||||
_ => Err(Error::new(400, format!("Invalid report modifier '{}'", s))),
|
_ => Err(CoreError::new(CoreErrorKind::InvalidInput, format!("Invalid report modifier '{}'", s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,13 +136,13 @@ pub enum AutomatedStationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for AutomatedStationType {
|
impl FromStr for AutomatedStationType {
|
||||||
type Err = Error;
|
type Err = CoreError;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"AO1" => Ok(AutomatedStationType::WithoutPrecipitationDiscriminator),
|
"AO1" => Ok(AutomatedStationType::WithoutPrecipitationDiscriminator),
|
||||||
"AO2" => Ok(AutomatedStationType::WithPrecipitationDiscriminator),
|
"AO2" => Ok(AutomatedStationType::WithPrecipitationDiscriminator),
|
||||||
_ => Err(Error::new(
|
_ => Err(CoreError::new(
|
||||||
400,
|
CoreErrorKind::InvalidInput,
|
||||||
format!("Invalid automated station type '{}'", s),
|
format!("Invalid automated station type '{}'", s),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
@@ -279,7 +278,7 @@ struct MetarRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MetarRow {
|
impl MetarRow {
|
||||||
async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
|
async fn insert(&self, pool: &Pool<Postgres>) -> CoreResult<()> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO {} (
|
INSERT INTO {} (
|
||||||
@@ -305,7 +304,7 @@ impl MetarRow {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_all(pool: &Pool<Postgres>, metars: Vec<Metar>) -> ApiResult<()> {
|
async fn insert_all(pool: &Pool<Postgres>, metars: Vec<Metar>) -> CoreResult<()> {
|
||||||
let chunk_size = 1000;
|
let chunk_size = 1000;
|
||||||
|
|
||||||
for chunk in metars.chunks(chunk_size) {
|
for chunk in metars.chunks(chunk_size) {
|
||||||
@@ -341,7 +340,7 @@ impl MetarRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Metar {
|
impl Metar {
|
||||||
fn parse_multiple(pool: &Pool<Postgres>, metar_strings: &Vec<&str>) -> ApiResult<Vec<Self>> {
|
fn parse_multiple(pool: &Pool<Postgres>, metar_strings: &Vec<&str>) -> CoreResult<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(pool, metar_string) {
|
match Self::parse(pool, metar_string) {
|
||||||
@@ -356,25 +355,26 @@ impl Metar {
|
|||||||
Ok(metars)
|
Ok(metars)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(pool: &Pool<Postgres>, metar_string: &str) -> ApiResult<Self> {
|
fn parse(pool: &Pool<Postgres>, metar_string: &str) -> CoreResult<Self> {
|
||||||
if metar_string.is_empty() {
|
if metar_string.is_empty() {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
404,
|
CoreErrorKind::InvalidInput,
|
||||||
"Unable to parse empty METAR data".to_string(),
|
"Unable to parse empty METAR data".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let metar_string = metar_string
|
||||||
|
.trim()
|
||||||
|
.trim_matches(|c| c == '"' || c == '\'' || c == '“' || c == '”' || c == '‘' || c == '’')
|
||||||
|
.trim();
|
||||||
|
|
||||||
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
|
let mut metar_parts: Vec<&str> = metar_string.split_whitespace().collect();
|
||||||
.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(CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
format!(
|
format!(
|
||||||
"Unable to parse METAR data in an unexpected format: {}",
|
"Unable to parse METAR data in an unexpected format: {}",
|
||||||
metar_string
|
metar_string
|
||||||
@@ -390,7 +390,8 @@ impl Metar {
|
|||||||
if metar_re.is_match(token) {
|
if metar_re.is_match(token) {
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
} else if speci_re.is_match(token) {
|
} else if speci_re.is_match(token) {
|
||||||
return Err(Error::new(500, format!("Unable to parse SPECI data: {}", metar_string)));
|
// TODO: Handle SPECI data
|
||||||
|
return Err(CoreError::new(CoreErrorKind::InvalidInput, format!("Unable to parse SPECI data: {}", metar_string)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Station Identifier
|
// Station Identifier
|
||||||
@@ -408,8 +409,8 @@ impl Metar {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
err.status,
|
CoreErrorKind::InvalidInput,
|
||||||
format!(
|
format!(
|
||||||
"Unexpected observation time field '{}': {}; {}",
|
"Unexpected observation time field '{}': {}; {}",
|
||||||
observation_time, metar_string, err
|
observation_time, metar_string, err
|
||||||
@@ -722,8 +723,8 @@ impl Metar {
|
|||||||
if metar_parts.is_empty() {
|
if metar_parts.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let slp_re = regex::Regex::new(r"^SLP([0-9]{3})$")?;
|
let slp_re = Regex::new(r"^SLP([0-9]{3})$")?;
|
||||||
let hourly_temp_re = regex::Regex::new(r"^T[01][0-9]{3}[01][0-9]{3}$")?;
|
let hourly_temp_re = Regex::new(r"^T[01][0-9]{3}[01][0-9]{3}$")?;
|
||||||
let remark = metar_parts[0];
|
let remark = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
if remark == "AO1" || remark == "AO2" {
|
if remark == "AO1" || remark == "AO2" {
|
||||||
@@ -757,8 +758,8 @@ impl Metar {
|
|||||||
minutes,
|
minutes,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
"Input string format is invalid".to_string(),
|
"Input string format is invalid".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -995,49 +996,45 @@ impl Metar {
|
|||||||
pub async fn get_cached_remote_metars(
|
pub async fn get_cached_remote_metars(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
etag: Option<String>,
|
etag: Option<String>,
|
||||||
) -> ApiResult<(Vec<Self>, String)> {
|
) -> CoreResult<(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 state.client.get(&url, etag.clone()).await {
|
let response = state.client.get(&url, etag.clone()).await?;
|
||||||
Ok(r) => {
|
let new_etag = response
|
||||||
let new_etag = r
|
.headers()
|
||||||
.headers()
|
.get(ETAG)
|
||||||
.get(ETAG)
|
.and_then(|h| h.to_str().ok())
|
||||||
.and_then(|h| h.to_str().ok())
|
.map(|s| s.to_string());
|
||||||
.map(|s| s.to_string());
|
|
||||||
|
|
||||||
let bytes = r.bytes().await?;
|
let bytes = response.bytes().await?;
|
||||||
let mut gz = GzDecoder::new(Cursor::new(bytes));
|
let mut gz = GzDecoder::new(Cursor::new(bytes));
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
gz.read_to_string(&mut text)?;
|
gz.read_to_string(&mut text)?;
|
||||||
|
|
||||||
let mut output: Vec<Metar> = Vec::new();
|
let mut output: Vec<Metar> = Vec::new();
|
||||||
|
|
||||||
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(&state.pool, 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);
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
match new_etag {
|
match new_etag {
|
||||||
Some(etag) => Ok((output, etag)),
|
Some(etag) => Ok((output, etag)),
|
||||||
None => match etag {
|
None => match etag {
|
||||||
Some(etag) => Ok((output, etag.to_string())),
|
Some(etag) => Ok((output, etag.to_string())),
|
||||||
None => Ok((output, String::new())),
|
None => Ok((output, String::new())),
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_remote_metars(state: &AppState, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
|
pub async fn get_remote_metars(state: &AppState, icaos: &Vec<String>) -> CoreResult<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
|
||||||
@@ -1063,7 +1060,7 @@ impl Metar {
|
|||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
|
Err(err) => return Err(CoreError::new(CoreErrorKind::InvalidInput, format!("METAR parse failed: {}", err))),
|
||||||
},
|
},
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
@@ -1072,12 +1069,12 @@ impl Metar {
|
|||||||
Ok(metars)
|
Ok(metars)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_row(row: MetarRow) -> ApiResult<Self> {
|
fn from_row(row: MetarRow) -> CoreResult<Self> {
|
||||||
let metar: Self = serde_json::from_value(row.data)?;
|
let metar: Self = serde_json::from_value(row.data)?;
|
||||||
Ok(metar)
|
Ok(metar)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_row(&self) -> ApiResult<MetarRow> {
|
fn to_row(&self) -> CoreResult<MetarRow> {
|
||||||
let data = serde_json::to_value(self)?;
|
let data = serde_json::to_value(self)?;
|
||||||
Ok(MetarRow {
|
Ok(MetarRow {
|
||||||
icao: self.icao.to_uppercase(),
|
icao: self.icao.to_uppercase(),
|
||||||
@@ -1087,7 +1084,7 @@ impl Metar {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_distinct(pool: &Pool<Postgres>, icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
|
pub async fn get_all_distinct(pool: &Pool<Postgres>, icao_list: &Vec<String>) -> CoreResult<Vec<Self>> {
|
||||||
if icao_list.is_empty() {
|
if icao_list.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
@@ -1113,7 +1110,7 @@ impl Metar {
|
|||||||
pub async fn get_or_update_metars(
|
pub async fn get_or_update_metars(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
icaos: &Vec<String>,
|
icaos: &Vec<String>,
|
||||||
) -> ApiResult<Vec<Self>> {
|
) -> CoreResult<Vec<Self>> {
|
||||||
let metars = Self::get_all_distinct(&state.pool, &icaos).await?;
|
let metars = Self::get_all_distinct(&state.pool, &icaos).await?;
|
||||||
let current_time = Utc::now().timestamp();
|
let current_time = Utc::now().timestamp();
|
||||||
|
|
||||||
@@ -1215,7 +1212,7 @@ impl Metar {
|
|||||||
Ok(updated_metars)
|
Ok(updated_metars)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_metars(state: &AppState, etag: Option<String>) -> ApiResult<String> {
|
pub async fn update_metars(state: &AppState, etag: Option<String>) -> CoreResult<String> {
|
||||||
let (remote_metars, etag) = Self::get_cached_remote_metars(state, etag)
|
let (remote_metars, etag) = Self::get_cached_remote_metars(state, etag)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
@@ -1227,7 +1224,7 @@ impl Metar {
|
|||||||
Ok(etag)
|
Ok(etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
|
pub async fn insert(&self, pool: &Pool<Postgres>) -> CoreResult<()> {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"Inserting metar {} with observation time {}",
|
"Inserting metar {} with observation time {}",
|
||||||
self.icao,
|
self.icao,
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::error::{ApiResult, Error};
|
use crate::error::{CoreError, CoreErrorKind, CoreResult};
|
||||||
use chrono::{Datelike, NaiveDate, Utc};
|
use chrono::{Datelike, NaiveDate, Utc};
|
||||||
|
|
||||||
pub fn parse_metar_time(observation_time: &str) -> ApiResult<String> {
|
pub fn parse_metar_time(observation_time: &str) -> CoreResult<String> {
|
||||||
if observation_time.len() != 7 {
|
if observation_time.len() != 7 {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
format!("Unable to parse observation time in {}", observation_time),
|
format!("Unable to parse observation time in {}", observation_time),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ pub fn parse_metar_time(observation_time: &str) -> ApiResult<String> {
|
|||||||
let current_month = current_time.month();
|
let current_month = current_time.month();
|
||||||
let candidate_date = NaiveDate::from_ymd_opt(current_year, current_month, observation_day)
|
let candidate_date = NaiveDate::from_ymd_opt(current_year, current_month, observation_day)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
Error::new(
|
CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
format!(
|
format!(
|
||||||
"Invalid date with day {} for current month",
|
"Invalid date with day {} for current month",
|
||||||
observation_day
|
observation_day
|
||||||
@@ -36,8 +36,8 @@ pub fn parse_metar_time(observation_time: &str) -> ApiResult<String> {
|
|||||||
let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) {
|
let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) {
|
||||||
Some(date) => date,
|
Some(date) => date,
|
||||||
None => {
|
None => {
|
||||||
return Err(Error::new(
|
return Err(CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
format!(
|
format!(
|
||||||
"Invalid time for time '{}': hour {}, minute {}",
|
"Invalid time for time '{}': hour {}, minute {}",
|
||||||
observation_time, observation_hour, observation_minute
|
observation_time, observation_hour, observation_minute
|
||||||
@@ -55,8 +55,8 @@ pub fn parse_metar_time(observation_time: &str) -> ApiResult<String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let adjusted_date = NaiveDate::from_ymd_opt(year, month, observation_day).ok_or_else(|| {
|
let adjusted_date = NaiveDate::from_ymd_opt(year, month, observation_day).ok_or_else(|| {
|
||||||
Error::new(
|
CoreError::new(
|
||||||
500,
|
CoreErrorKind::InvalidInput,
|
||||||
format!(
|
format!(
|
||||||
"Invalid date with day {} for month {}",
|
"Invalid date with day {} for month {}",
|
||||||
observation_day, month
|
observation_day, month
|
||||||
@@ -7,7 +7,7 @@ use s3::{Bucket, BucketConfiguration, Region};
|
|||||||
use s3::creds::Credentials;
|
use s3::creds::Credentials;
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use crate::error::ApiResult;
|
use crate::error::CoreResult;
|
||||||
use crate::http_client::HttpClient;
|
use crate::http_client::HttpClient;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -19,7 +19,7 @@ pub struct AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub async fn new() -> ApiResult<Self> {
|
pub async fn new() -> CoreResult<Self> {
|
||||||
let client = HttpClient::default()?;
|
let client = HttpClient::default()?;
|
||||||
|
|
||||||
let pool: Pool<Postgres> = {
|
let pool: Pool<Postgres> = {
|
||||||
@@ -139,19 +139,19 @@ impl AppState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set(&self, key: &str, value: &str) -> ApiResult<()> {
|
pub async fn set(&self, key: &str, value: &str) -> CoreResult<()> {
|
||||||
let mut connection_manager = self.connection_manager.lock()?;
|
let mut connection_manager = self.connection_manager.lock()?;
|
||||||
connection_manager.set(key, value).await?;
|
connection_manager.set(key, value).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_ex(&self, key: &str, value: &str, seconds: u64) -> ApiResult<()> {
|
pub async fn set_ex(&self, key: &str, value: &str, seconds: u64) -> CoreResult<()> {
|
||||||
let mut connection_manager = self.connection_manager.lock()?;
|
let mut connection_manager = self.connection_manager.lock()?;
|
||||||
connection_manager.set_ex(key, value, seconds).await?;
|
connection_manager.set_ex(key, value, seconds).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&self, key: &str) -> ApiResult<Option<String>> {
|
pub async fn get(&self, key: &str) -> CoreResult<Option<String>> {
|
||||||
let mut connection_manager = self.connection_manager.lock()?;
|
let mut connection_manager = self.connection_manager.lock()?;
|
||||||
match connection_manager.get(key).await {
|
match connection_manager.get(key).await {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
@@ -159,7 +159,7 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn del(&self, key: &str) -> ApiResult<()> {
|
pub async fn del(&self, key: &str) -> CoreResult<()> {
|
||||||
let mut connection_manager = self.connection_manager.lock()?;
|
let mut connection_manager = self.connection_manager.lock()?;
|
||||||
connection_manager.del(key).await?;
|
connection_manager.del(key).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
11
crates/scheduler/Cargo.toml
Normal file
11
crates/scheduler/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "scheduler"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lib = { path = "../lib" }
|
||||||
|
chrono = "0.4.42"
|
||||||
|
tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread"] }
|
||||||
|
log = "0.4.28"
|
||||||
|
env_logger = "0.11.8"
|
||||||
24
crates/scheduler/Dockerfile
Normal file
24
crates/scheduler/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# =========
|
||||||
|
# Builder
|
||||||
|
# =========
|
||||||
|
FROM rust:bookworm AS builder
|
||||||
|
WORKDIR /builder
|
||||||
|
|
||||||
|
COPY crates/lib /lib
|
||||||
|
COPY crates/scheduler/src ./src
|
||||||
|
COPY crates/scheduler/Cargo.toml ./
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y cmake
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# =========
|
||||||
|
# Runtime
|
||||||
|
# =========
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
WORKDIR /scheduler
|
||||||
|
RUN apt-get update && apt-get install -y openssl libpq-dev ca-certificates
|
||||||
|
USER root
|
||||||
|
|
||||||
|
COPY --from=builder /builder/target/release/scheduler /usr/local/bin/scheduler
|
||||||
|
|
||||||
|
CMD ["scheduler"]
|
||||||
56
crates/scheduler/src/main.rs
Normal file
56
crates/scheduler/src/main.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::env;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use env_logger::Builder;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use tokio::time::interval;
|
||||||
|
use lib::metars::Metar;
|
||||||
|
use lib::state::AppState;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
Builder::new()
|
||||||
|
.filter_level(LevelFilter::Info) // Set a default log level
|
||||||
|
.filter_module("scheduler", LevelFilter::Trace)
|
||||||
|
.filter_module("lib", LevelFilter::Trace)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let state = match AppState::new().await {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to create state: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let seconds = env::var("METAR_INTERVAL")
|
||||||
|
.unwrap_or("300".to_string())
|
||||||
|
.parse::<u64>()
|
||||||
|
.unwrap_or(300);
|
||||||
|
|
||||||
|
// Create an interval ticker
|
||||||
|
let mut interval = interval(Duration::from_secs(seconds));
|
||||||
|
let mut etag = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
// Record start times
|
||||||
|
let start_monotonic = Instant::now();
|
||||||
|
let start_utc: DateTime<Utc> = Utc::now();
|
||||||
|
log::debug!("METAR update started at {}", start_utc);
|
||||||
|
|
||||||
|
// Run the update
|
||||||
|
match Metar::update_metars(&state, etag.clone()).await {
|
||||||
|
Ok(new_etag) => etag = Some(new_etag),
|
||||||
|
Err(err) => log::error!("METAR update failed: {}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start_monotonic.elapsed();
|
||||||
|
let next_utc = Utc::now() + chrono::Duration::from_std(Duration::from_secs(seconds)).unwrap();
|
||||||
|
log::info!(
|
||||||
|
"METAR update finished in {:.2?}; next run at {}",
|
||||||
|
elapsed,
|
||||||
|
next_utc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
container_name: aviation-nginx
|
container_name: aviation-nginx
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: nginx/Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
environment:
|
environment:
|
||||||
SSL_CERT_PATH: /etc/nginx/ssl/localhost.crt
|
SSL_CERT_PATH: /etc/nginx/ssl/localhost.crt
|
||||||
@@ -84,7 +84,7 @@ services:
|
|||||||
container_name: aviation-api
|
container_name: aviation-api
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: crates/api/Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
environment:
|
environment:
|
||||||
SSL_CA_PATH: /ssl/ca.pem
|
SSL_CA_PATH: /ssl/ca.pem
|
||||||
@@ -109,6 +109,22 @@ services:
|
|||||||
- api
|
- api
|
||||||
<<: *default_restart
|
<<: *default_restart
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
image: gitea.bensherriff.com/bsherriff/aviation-scheduler:latest
|
||||||
|
container_name: aviation-scheduler
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: crates/scheduler/Dockerfile
|
||||||
|
env_file: *env
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST: aviation-postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
profiles:
|
||||||
|
- api
|
||||||
|
<<: *default_restart
|
||||||
|
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit
|
image: axllent/mailpit
|
||||||
container_name: mailpit
|
container_name: mailpit
|
||||||
|
|||||||
Reference in New Issue
Block a user