refactor auth, maybe bug with hashing
This commit is contained in:
13
.env
13
.env
@@ -1,12 +1,13 @@
|
||||
RUST_LOG=warn,service=info
|
||||
|
||||
DATABASE_CONTAINER=aviation-db
|
||||
DATABASE_USER=aviation
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_NAME=aviation
|
||||
DATABASE_HOST=db
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
MINIO_ROOT_USER=aviation
|
||||
@@ -15,11 +16,9 @@ MINIO_HOST=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_PORT_INTERNAL=9001
|
||||
|
||||
SERVICE_HOST=service
|
||||
SERVICE_HOST=localhost
|
||||
SERVICE_PORT=5000
|
||||
|
||||
KEYS_DIR_PATH=
|
||||
ACCESS_TOKEN_MAXAGE=5
|
||||
REFRESH_TOKEN_MAXAGE=30
|
||||
UI_PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
||||
|
||||
52
Makefile
52
Makefile
@@ -14,29 +14,39 @@ help: ## This info
|
||||
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
|
||||
build: ## Build Docker containers
|
||||
docker compose build
|
||||
format: ## Format code
|
||||
@cd service && cargo fmt
|
||||
@cd ui && npm run format
|
||||
|
||||
tag: ## Tag Docker images
|
||||
docker tag aviation-ui:latest aviation-ui:${GIT_HASH}
|
||||
docker tag aviation-service:latest aviation-service:${GIT_HASH}
|
||||
backend-up: ## Start Docker containers
|
||||
@docker compose --profile backend up -d
|
||||
|
||||
up: ## Start Docker containers
|
||||
docker compose up -d
|
||||
up-backend: backend-up
|
||||
|
||||
down: ## Stop Docker containers
|
||||
docker compose down
|
||||
backend-down: ## Stop Docker containers
|
||||
@docker compose --profile backend down
|
||||
|
||||
clean: ## Cleanup Docker containers
|
||||
docker compose down && \
|
||||
docker image rm aviation-ui || \
|
||||
docker image rm aviation-service || \
|
||||
docker network rm aviation-frontend || \
|
||||
docker network rm aviation-backend
|
||||
down-backend: backend-down
|
||||
|
||||
generate: ## Generate RSA keys
|
||||
mkdir keys
|
||||
openssl genrsa -out keys/access_private_key.pem 4096
|
||||
openssl rsa -in keys/access_private_key.pem -pubout -outform PEM -out keys/access_public_key.pem
|
||||
openssl genrsa -out keys/refresh_private_key.pem 4096
|
||||
openssl rsa -in keys/refresh_private_key.pem -pubout -outform PEM -out keys/refresh_public_key.pem
|
||||
run: ## Run the api
|
||||
@cd service && cargo run
|
||||
|
||||
frontend-up: ## Start Docker containers
|
||||
@docker compose --profile frontend up -d
|
||||
|
||||
up-frontend: frontend-up
|
||||
|
||||
frontend-down: ## Stop Docker containers
|
||||
@docker compose --profile frontend down
|
||||
|
||||
down-frontend: frontend-down
|
||||
|
||||
docker-clean: ## Stop the docker containers and remove volumes
|
||||
@echo "Stopping docker container and removing volumes..."
|
||||
@docker compose --profile frontend --profile api --profile backend down -v
|
||||
@echo "Docker container stopped and volumes removed"
|
||||
|
||||
docker-refresh: docker-clean up-backend ## Refresh the database
|
||||
|
||||
psql: ## Connect to the PSQL DB
|
||||
@docker exec -it ${DATABASE_CONTAINER} psql -U ${DATABASE_USER} -P pager=off
|
||||
@@ -1,12 +1,15 @@
|
||||
version: '3'
|
||||
x-env_file: &env
|
||||
- path: .env
|
||||
required: true
|
||||
- path: .env.local
|
||||
required: false
|
||||
|
||||
name: aviation
|
||||
services:
|
||||
db:
|
||||
image: postgis/postgis:latest
|
||||
container_name: aviation-db
|
||||
env_file:
|
||||
- .env
|
||||
env_file: *env
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USER}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
@@ -18,7 +21,10 @@ services:
|
||||
- "${DATABASE_PORT:-5432}:5432"
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: aviation-redis
|
||||
@@ -28,7 +34,10 @@ services:
|
||||
- ${REDIS_PORT:-6379}:6379
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: aviation-minio
|
||||
@@ -42,17 +51,14 @@ services:
|
||||
- ${MINIO_PORT_INTERNAL:-9001}:9001
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
command: server --console-address ":9001" /data
|
||||
restart: unless-stopped
|
||||
|
||||
service:
|
||||
container_name: aviation-service
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
KEYS_DIR_PATH: /keys
|
||||
volumes:
|
||||
- ${KEYS_DIR_PATH}:/keys
|
||||
api:
|
||||
container_name: aviation-api
|
||||
env_file: *env
|
||||
ports:
|
||||
- "${SERVICE_PORT:-5000}:5000"
|
||||
build:
|
||||
@@ -64,22 +70,29 @@ services:
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
profiles:
|
||||
- api
|
||||
restart: unless-stopped
|
||||
|
||||
ui:
|
||||
container_name: aviation-ui
|
||||
env_file:
|
||||
- .env
|
||||
env_file: *env
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
ports:
|
||||
- ${UI_PORT:-3000}:3000
|
||||
build:
|
||||
context: ui
|
||||
depends_on:
|
||||
- service
|
||||
context: ./ui/
|
||||
target: dev
|
||||
volumes:
|
||||
- ./ui/src:/app/src
|
||||
- ./ui/public:/app/public
|
||||
- ./ui/styles:/app/styles
|
||||
networks:
|
||||
- frontend
|
||||
profiles:
|
||||
- frontend
|
||||
command: ["npm", "run", "dev"]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
27
service/.env
27
service/.env
@@ -1,27 +0,0 @@
|
||||
RUST_LOG=warn,service=debug
|
||||
|
||||
DATABASE_CONTAINER=aviation-db
|
||||
|
||||
DATABASE_USER=aviation
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_NAME=aviation
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
MINIO_ROOT_USER=aviation
|
||||
MINIO_ROOT_PASSWORD=
|
||||
MINIO_HOST=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_PORT_INTERNAL=9001
|
||||
|
||||
SERVICE_HOST=localhost
|
||||
SERVICE_PORT=5000
|
||||
|
||||
KEYS_DIR_PATH=
|
||||
ACCESS_TOKEN_MAXAGE=240
|
||||
REFRESH_TOKEN_MAXAGE=1440
|
||||
|
||||
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
||||
23
service/Cargo.lock
generated
23
service/Cargo.lock
generated
@@ -547,9 +547,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
@@ -2206,18 +2206,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.204"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2286,6 +2286,8 @@ dependencies = [
|
||||
"log",
|
||||
"postgis_diesel",
|
||||
"r2d2",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"redis",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -2698,11 +2700,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.37"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
@@ -2710,9 +2711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.31"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@@ -36,3 +36,5 @@ ahash = "0.8.11" # https://github.com/tkaitchuck/aHash/issues/200
|
||||
regex = "1.10.5"
|
||||
futures-util = "0.3.30"
|
||||
rust-s3 = "0.34.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!make
|
||||
SHELL := /bin/bash
|
||||
|
||||
GIT_HASH ?= $(shell git log --format="%h" -n 1)
|
||||
|
||||
include .env
|
||||
-include .env.local
|
||||
export
|
||||
|
||||
.PHONY: help build start stop lint
|
||||
|
||||
help: ## This info
|
||||
@echo
|
||||
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
|
||||
format: ## Format code
|
||||
@echo "Formatting code..."
|
||||
@cargo fmt
|
||||
@echo "Format complete"
|
||||
|
||||
run: ## Run the service
|
||||
@cargo run
|
||||
|
||||
clean: ## Cleanup
|
||||
@echo "Cleaning up..."
|
||||
@cargo clean
|
||||
@rm -rf ../keys
|
||||
@echo "Cleanup complete"
|
||||
|
||||
up: ## Start the Docker containers
|
||||
@docker compose --profile backend up -d
|
||||
|
||||
down: ## Stop the Docker containers
|
||||
@docker compose --profile backend down
|
||||
|
||||
connect: ## Connect to the PSQL DB
|
||||
@docker exec -it ${DATABASE_CONTAINER} psql -U postgres
|
||||
|
||||
docker-build: ## Build the Docker image
|
||||
@docker compose build
|
||||
|
||||
docker-tag: ## Tag the Docker image
|
||||
@docker tag aviation-service:latest aviation-service:${GIT_HASH}
|
||||
|
||||
docker-run: ## Start the service
|
||||
@docker compose --profile service up -d
|
||||
|
||||
docker-clean: ## Cleanup Docker containers
|
||||
@docker compose --profile backend --profile service down -v
|
||||
|
||||
clean-db: ## Remove database
|
||||
@docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"'
|
||||
@docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true
|
||||
|
||||
generate-keys: ## Generate RSA keys
|
||||
@mkdir ../keys/
|
||||
@openssl genrsa -out ../keys/access_private_key.pem 4096
|
||||
@openssl rsa -in ../keys/access_private_key.pem -pubout -outform PEM -out ../keys/access_public_key.pem
|
||||
@openssl genrsa -out ../keys/refresh_private_key.pem 4096
|
||||
@openssl rsa -in ../keys/refresh_private_key.pem -pubout -outform PEM -out ../keys/refresh_public_key.pem
|
||||
|
||||
psql: ## Connect to the PSQL DB
|
||||
@docker exec -it ${DATABASE_CONTAINER} psql -U ${DATABASE_USER}
|
||||
@@ -1,93 +0,0 @@
|
||||
x-env_file: &env
|
||||
- path: .env
|
||||
required: true
|
||||
- path: .env.local
|
||||
required: false
|
||||
|
||||
name: aviation
|
||||
services:
|
||||
db:
|
||||
image: postgis/postgis:latest
|
||||
container_name: aviation-db
|
||||
env_file: *env
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USER}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
- db_logs:/var/log
|
||||
ports:
|
||||
- "${DATABASE_PORT:-5432}:5432"
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: aviation-redis
|
||||
volumes:
|
||||
- redis:/data
|
||||
ports:
|
||||
- ${REDIS_PORT:-6379}:6379
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: aviation-minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- minio:/data
|
||||
ports:
|
||||
- ${MINIO_PORT:-9000}:9000
|
||||
- ${MINIO_PORT_INTERNAL:-9001}:9001
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- backend
|
||||
command: server --console-address ":9001" /data
|
||||
restart: unless-stopped
|
||||
|
||||
service:
|
||||
container_name: aviation-service
|
||||
env_file: *env
|
||||
environment:
|
||||
DATABASE_HOST: db
|
||||
DATABASE_PORT: 5432
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
SERVICE_HOST: service
|
||||
SERVICE_PORT: 5000
|
||||
KEYS_DIR_PATH: /keys
|
||||
volumes:
|
||||
- ${KEYS_DIR_PATH}:/keys
|
||||
ports:
|
||||
- "${SERVICE_PORT:-5000}:5000"
|
||||
build:
|
||||
context: .
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
- minio
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
profiles:
|
||||
- service
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
db:
|
||||
db_logs:
|
||||
redis:
|
||||
minio:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
backend:
|
||||
@@ -2,7 +2,7 @@ use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::db;
|
||||
use crate::error_handler::ServiceError;
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::db::schema::airports;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_query;
|
||||
@@ -218,7 +218,7 @@ impl FromStr for QueryOrderField {
|
||||
}
|
||||
|
||||
impl QueryAirport {
|
||||
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
||||
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> ApiResult<Vec<Self>> {
|
||||
let mut conn = db::connection()?;
|
||||
let mut query: String = "SELECT * FROM airports".to_string();
|
||||
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
|
||||
@@ -258,7 +258,7 @@ impl QueryAirport {
|
||||
let airports: Vec<QueryAirport> = match sql_query(query).load(&mut conn) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
return Err(ServiceError {
|
||||
return Err(ApiError {
|
||||
status: 500,
|
||||
message: format!("{}", err),
|
||||
})
|
||||
@@ -267,7 +267,7 @@ impl QueryAirport {
|
||||
Ok(airports)
|
||||
}
|
||||
|
||||
pub fn get_count(filters: &QueryFilters) -> Result<i64, ServiceError> {
|
||||
pub fn get_count(filters: &QueryFilters) -> ApiResult<i64> {
|
||||
let mut conn = db::connection()?;
|
||||
let mut query = "SELECT COUNT(*) FROM airports".to_string();
|
||||
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
|
||||
@@ -287,7 +287,7 @@ impl QueryAirport {
|
||||
let count: Vec<Count> = match sql_query(query).load(&mut conn) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
return Err(ServiceError {
|
||||
return Err(ApiError {
|
||||
status: 500,
|
||||
message: format!("{}", err),
|
||||
})
|
||||
@@ -297,14 +297,14 @@ impl QueryAirport {
|
||||
}
|
||||
|
||||
// TODO: Unsafe query, need to sanitize inputs
|
||||
fn build_filter_query(filters: &QueryFilters) -> Result<String, ServiceError> {
|
||||
fn build_filter_query(filters: &QueryFilters) -> ApiResult<String> {
|
||||
let mut query = "".to_string();
|
||||
let mut parts: Vec<String> = vec![];
|
||||
|
||||
if let Some(bounds) = &filters.bounds {
|
||||
// convert bounds to a WKT polygon
|
||||
if bounds.rings.len() > 1 {
|
||||
return Err(ServiceError {
|
||||
return Err(ApiError {
|
||||
status: 400,
|
||||
message: "Only one polygon is allowed".to_string(),
|
||||
});
|
||||
@@ -376,7 +376,7 @@ impl QueryAirport {
|
||||
return Ok(query);
|
||||
}
|
||||
|
||||
pub fn get(icao: &str) -> Result<Self, ServiceError> {
|
||||
pub fn get(icao: &str) -> ApiResult<Self> {
|
||||
let mut conn = db::connection()?;
|
||||
let airport = airports::table
|
||||
.filter(airports::icao.eq(icao))
|
||||
@@ -384,7 +384,7 @@ impl QueryAirport {
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
pub fn insert(airport: Self) -> Result<Self, ServiceError> {
|
||||
pub fn insert(airport: Self) -> ApiResult<Self> {
|
||||
let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
|
||||
db::connection()?;
|
||||
let airport = Self::from(airport);
|
||||
@@ -395,7 +395,7 @@ impl QueryAirport {
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
pub fn insert_all(airports: Vec<Self>) -> Result<Vec<Self>, ServiceError> {
|
||||
pub fn insert_all(airports: Vec<Self>) -> ApiResult<Vec<Self>> {
|
||||
let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
|
||||
db::connection()?;
|
||||
let mut inserted_airports: Vec<Self> = vec![];
|
||||
@@ -410,7 +410,7 @@ impl QueryAirport {
|
||||
Ok(inserted_airports)
|
||||
}
|
||||
|
||||
pub fn update(airport: Self) -> Result<Self, ServiceError> {
|
||||
pub fn update(airport: Self) -> ApiResult<Self> {
|
||||
let mut conn = db::connection()?;
|
||||
let airport = diesel::update(airports::table)
|
||||
.filter(airports::icao.eq(airport.icao.clone()))
|
||||
@@ -419,7 +419,7 @@ impl QueryAirport {
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
pub fn delete(icao: Option<String>) -> Result<usize, ServiceError> {
|
||||
pub fn delete(icao: Option<String>) -> ApiResult<usize> {
|
||||
let mut conn = db::connection()?;
|
||||
let res = match icao {
|
||||
Some(icao) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures_util::stream::StreamExt as _;
|
||||
use crate::{
|
||||
airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport, AirportCategory},
|
||||
db::{Response, Metadata},
|
||||
auth::{JwtAuth, verify_role},
|
||||
auth::{Auth, verify_role},
|
||||
};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||
@@ -26,7 +26,7 @@ struct AirportsQuery {
|
||||
}
|
||||
|
||||
#[post("/import")]
|
||||
async fn import_airports(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
|
||||
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
if let Err(err) = verify_role(&auth, "admin") {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
@@ -193,11 +193,7 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
HttpResponse::Ok().json(Response {
|
||||
data: airports,
|
||||
meta: Some(Metadata {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
}),
|
||||
meta: Some(Metadata { page, limit, total }),
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -222,7 +218,7 @@ async fn get_airport(icao: web::Path<String>) -> HttpResponse {
|
||||
}
|
||||
|
||||
#[post("")]
|
||||
async fn create_airport(airport: web::Json<Airport>, auth: JwtAuth) -> HttpResponse {
|
||||
async fn create_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, "admin") {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
@@ -244,7 +240,7 @@ async fn create_airport(airport: web::Json<Airport>, auth: JwtAuth) -> HttpRespo
|
||||
async fn update_airport(
|
||||
_icao: web::Path<String>,
|
||||
airport: web::Json<Airport>,
|
||||
auth: JwtAuth,
|
||||
auth: Auth,
|
||||
) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, "admin") {
|
||||
Ok(_) => {}
|
||||
@@ -264,7 +260,7 @@ async fn update_airport(
|
||||
}
|
||||
|
||||
#[delete("")]
|
||||
async fn delete_airports(auth: JwtAuth) -> HttpResponse {
|
||||
async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, "admin") {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
@@ -279,7 +275,7 @@ async fn delete_airports(auth: JwtAuth) -> HttpResponse {
|
||||
}
|
||||
|
||||
#[delete("/{icao}")]
|
||||
async fn delete_airport(icao: web::Path<String>, auth: JwtAuth) -> HttpResponse {
|
||||
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, "admin") {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
|
||||
@@ -1,111 +1,58 @@
|
||||
use std::env;
|
||||
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError,
|
||||
},
|
||||
Argon2, PasswordHash,
|
||||
password_hash::{rand_core::OsRng, SaltString},
|
||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
mod model;
|
||||
mod routes;
|
||||
mod session;
|
||||
|
||||
pub use model::*;
|
||||
pub use session::*;
|
||||
pub use routes::init_routes;
|
||||
use crate::error_handler::ServiceError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TokenClaims {
|
||||
sub: String, // Subject
|
||||
token_uuid: String, // Token UUID
|
||||
iss: String, // Issuer
|
||||
exp: i64, // Expiration time
|
||||
iat: i64, // Issued At
|
||||
nbf: i64, // Not Before
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
|
||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||
|
||||
pub fn csprng_128bit(take: usize) -> String {
|
||||
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
||||
let rng = ChaCha20Rng::from_entropy();
|
||||
rng
|
||||
.sample_iter(rand::distributions::Alphanumeric)
|
||||
.take(take)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TokenDetails {
|
||||
pub token: Option<String>,
|
||||
pub token_uuid: uuid::Uuid,
|
||||
pub email: String,
|
||||
pub expires_in: Option<i64>,
|
||||
}
|
||||
|
||||
pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
|
||||
let validation = Validation::new(Algorithm::RS256);
|
||||
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
||||
let email = decoded.claims.sub;
|
||||
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
||||
Ok(TokenDetails {
|
||||
token: None,
|
||||
token_uuid,
|
||||
email,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||
let access_private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?;
|
||||
generate_token(&email, access_token_max_age, &access_private_key)
|
||||
}
|
||||
|
||||
pub fn generate_refresh_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||
let refresh_private_key =
|
||||
std::fs::read_to_string(format!("{}/refresh_private_key.pem", keys_dir))?;
|
||||
generate_token(&email, refresh_token_max_age, &refresh_private_key)
|
||||
}
|
||||
|
||||
pub fn generate_token(
|
||||
email: &str,
|
||||
ttl: i64,
|
||||
private_key: &str,
|
||||
) -> Result<TokenDetails, ServiceError> {
|
||||
let now = chrono::Utc::now();
|
||||
let mut token_details = TokenDetails {
|
||||
token: None,
|
||||
token_uuid: uuid::Uuid::new_v4(),
|
||||
email: email.to_string(),
|
||||
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()),
|
||||
};
|
||||
let claims = TokenClaims {
|
||||
sub: token_details.email.clone(),
|
||||
iss: "aviation-weather".to_string(),
|
||||
token_uuid: token_details.token_uuid.to_string(),
|
||||
exp: token_details.expires_in.unwrap(),
|
||||
iat: now.timestamp(),
|
||||
nbf: now.timestamp(),
|
||||
};
|
||||
let header = Header::new(Algorithm::RS256);
|
||||
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
|
||||
let token = encode(&header, &claims, &key)?;
|
||||
token_details.token = Some(token);
|
||||
Ok(token_details)
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &[u8]) -> Result<String, HashError> {
|
||||
pub fn hash(str: &str) -> ApiResult<String> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Ok(
|
||||
Argon2::default()
|
||||
.hash_password(password, &salt)?
|
||||
.to_string(),
|
||||
)
|
||||
let bytes = str.as_bytes();
|
||||
let hash = Argon2::default().hash_password(bytes, &salt)?.to_string();
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
pub fn verify_password(hash: &str, password: &[u8]) -> Result<(), HashError> {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
Ok(Argon2::default().verify_password(password, &parsed_hash)?)
|
||||
pub fn verify_hash(str: &str, hash: &str) -> bool {
|
||||
let bytes = str.as_bytes();
|
||||
let parsed_hash = match PasswordHash::new(hash) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return false,
|
||||
};
|
||||
match Argon2::default().verify_password(bytes, &parsed_hash) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_role(auth: &Auth, role: &str) -> ApiResult<()> {
|
||||
if auth.user.role == role {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError {
|
||||
status: 403,
|
||||
message: "User does not have permission to perform this action.".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +1,29 @@
|
||||
use std::{
|
||||
future::{ready, Ready},
|
||||
env,
|
||||
};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
|
||||
use diesel::prelude::*;
|
||||
use log::error;
|
||||
use redis::Commands;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::error_handler::ServiceError;
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
users::{User, UserResponse},
|
||||
};
|
||||
|
||||
use crate::db::{schema::users, connection};
|
||||
|
||||
use super::{hash_password, verify_token};
|
||||
use super::{Session, SESSION_COOKIE_NAME};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterUser {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub struct Auth {
|
||||
pub session_id: Option<String>,
|
||||
pub user: UserResponse,
|
||||
}
|
||||
|
||||
impl RegisterUser {
|
||||
pub fn convert_to_insert(self) -> Result<InsertUser, ServiceError> {
|
||||
let hash = hash_password(self.password.as_bytes())?;
|
||||
Ok(InsertUser {
|
||||
email: self.email.to_lowercase(),
|
||||
hash,
|
||||
role: "user".to_string(),
|
||||
first_name: self.first_name,
|
||||
last_name: self.last_name,
|
||||
updated_at: chrono::Utc::now().naive_utc(),
|
||||
created_at: chrono::Utc::now().naive_utc(),
|
||||
profile_picture: None,
|
||||
favorites: vec![],
|
||||
verified: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable, QueryableByName, Serialize, Deserialize)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct QueryUser {
|
||||
pub email: String,
|
||||
pub hash: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub profile_picture: Option<String>,
|
||||
pub favorites: Vec<String>,
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl QueryUser {
|
||||
pub fn get_by_email(email: &str) -> Result<QueryUser, ServiceError> {
|
||||
let mut conn = connection()?;
|
||||
// Check if the user exists by email, case insensitive
|
||||
|
||||
let user = users::table
|
||||
.filter(users::email.eq(email.to_lowercase()))
|
||||
.first(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct InsertUser {
|
||||
pub email: String,
|
||||
pub hash: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub profile_picture: Option<String>,
|
||||
pub favorites: Vec<String>,
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl InsertUser {
|
||||
pub fn insert(user: Self) -> Result<QueryUser, ServiceError> {
|
||||
let mut conn = connection()?;
|
||||
let user = diesel::insert_into(users::table)
|
||||
.values(user)
|
||||
.get_result(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub fn update_profile_picture(
|
||||
email: &str,
|
||||
profile_picture: Option<&str>,
|
||||
) -> Result<QueryUser, ServiceError> {
|
||||
let mut conn = connection()?;
|
||||
let user = diesel::update(users::table)
|
||||
.filter(users::email.eq(&email))
|
||||
.set(users::profile_picture.eq(profile_picture))
|
||||
.get_result(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub fn update_favorites(email: &str, favorites: Vec<String>) -> Result<QueryUser, ServiceError> {
|
||||
let mut conn = connection()?;
|
||||
let user = diesel::update(users::table)
|
||||
.filter(users::email.eq(&email))
|
||||
.set(users::favorites.eq(favorites))
|
||||
.get_result(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseUser {
|
||||
pub email: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub profile_picture: Option<String>,
|
||||
}
|
||||
|
||||
impl From<QueryUser> for ResponseUser {
|
||||
fn from(user: QueryUser) -> Self {
|
||||
ResponseUser {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
profile_picture: user.profile_picture,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JwtAuth {
|
||||
pub token: uuid::Uuid,
|
||||
pub user: ResponseUser,
|
||||
}
|
||||
|
||||
impl FromRequest for JwtAuth {
|
||||
impl FromRequest for Auth {
|
||||
type Error = ActixError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let access_token = match req
|
||||
.cookie("access_token")
|
||||
// Get session ID from request
|
||||
let session_id = match req
|
||||
.cookie(SESSION_COOKIE_NAME)
|
||||
.map(|c| c.value().to_string())
|
||||
.or_else(|| {
|
||||
req
|
||||
@@ -158,75 +31,37 @@ impl FromRequest for JwtAuth {
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.map(|h| h.to_str().unwrap().split_at(7).1.to_string())
|
||||
}) {
|
||||
Some(token) => token,
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
let fut = async {
|
||||
Err(
|
||||
ApiError {
|
||||
status: 401,
|
||||
message: "Unauthorized".to_string(),
|
||||
})))
|
||||
message: "No session ID found in the request".to_string(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
return Box::pin(fut);
|
||||
}
|
||||
};
|
||||
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||
.expect("Failed to read access public key");
|
||||
// Get IP address from request
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
let access_token_details = match verify_token(&access_token, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to verify access token: {}", err);
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("Failed to verify access token: {}", err),
|
||||
})));
|
||||
}
|
||||
};
|
||||
|
||||
let access_token_uuid =
|
||||
uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||
|
||||
let mut conn = match crate::db::redis_connection() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to get redis connection: {}", err),
|
||||
})));
|
||||
}
|
||||
};
|
||||
let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("Access token was not found"),
|
||||
})))
|
||||
}
|
||||
};
|
||||
|
||||
match QueryUser::get_by_email(&user_email) {
|
||||
Ok(user) => ready(Ok(JwtAuth {
|
||||
token: access_token_uuid,
|
||||
// Verify the session
|
||||
let fut = async move {
|
||||
match Session::verify(&session_id, &ip_address).await {
|
||||
Ok(session) => match User::get_by_email(&session.email) {
|
||||
Ok(user) => Ok(Auth {
|
||||
session_id: Some(session_id),
|
||||
user: user.into(),
|
||||
})),
|
||||
Err(_) => {
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("User was not found"),
|
||||
})))
|
||||
}
|
||||
}),
|
||||
Err(err) => Err(err.into()),
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_role(auth: &JwtAuth, role: &str) -> Result<(), ServiceError> {
|
||||
if auth.user.role == role {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ServiceError {
|
||||
status: 403,
|
||||
message: "Forbidden".to_string(),
|
||||
})
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
use std::env;
|
||||
|
||||
use actix_web::{
|
||||
get, post, web, HttpResponse, ResponseError,
|
||||
post, web, HttpResponse, ResponseError,
|
||||
cookie::{Cookie, time::Duration},
|
||||
HttpRequest,
|
||||
};
|
||||
use log::error;
|
||||
use redis::AsyncCommands;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::{error_handler::ServiceError, db::Response};
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token,
|
||||
generate_access_token, generate_refresh_token,
|
||||
},
|
||||
db,
|
||||
auth::{verify_hash, Session, SESSION_COOKIE_NAME},
|
||||
error::ApiError,
|
||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||
};
|
||||
|
||||
use crate::auth::Auth;
|
||||
|
||||
#[post("/register")]
|
||||
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||
async fn register(user: web::Json<RegisterRequest>) -> HttpResponse {
|
||||
let register_user = user.0;
|
||||
let insert_user: InsertUser = match register_user.convert_to_insert() {
|
||||
let insert_user: User = match register_user.to_user() {
|
||||
Ok(user) => user,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
match InsertUser::insert(insert_user) {
|
||||
Ok(_) => HttpResponse::Created().finish(),
|
||||
match User::insert(insert_user) {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
HttpResponse::Created().json(response)
|
||||
},
|
||||
Err(err) => {
|
||||
// Obfuscate the service error message to prevent leaking database details
|
||||
if err.status == 409 {
|
||||
@@ -39,430 +35,64 @@ async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let email = request.email.clone();
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
let query_user = match QueryUser::get_by_email(&email) {
|
||||
let query_user = match User::get_by_email(&email) {
|
||||
Ok(query_user) => query_user,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
let hash = &query_user.hash;
|
||||
let password = request.password.as_bytes();
|
||||
match verify_password(hash, password) {
|
||||
Ok(_) => {
|
||||
let access_token_details = match generate_access_token(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate access token: {}", err);
|
||||
log::error!("{}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token_details = match generate_refresh_token(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate refresh token: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
if verify_hash(&query_user.hash, &request.password) {
|
||||
// Create a session
|
||||
let session = Session::new(&email, &ip_address);
|
||||
let session_cookie = session.cookie();
|
||||
// Save the session to the database
|
||||
if let Err(err) = session.store().await {
|
||||
log::error!("Failed to store session");
|
||||
return ResponseError::error_response(&ApiError::new(500, err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut conn = match db::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn
|
||||
.set_ex(
|
||||
access_token_details.token_uuid.to_string(),
|
||||
&email,
|
||||
(access_token_max_age * 60) as u64,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = access_result {
|
||||
error!("Failed to set access token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to set access token in redis: {}", err),
|
||||
});
|
||||
};
|
||||
|
||||
let refresh_result: redis::RedisResult<()> = conn
|
||||
.set_ex(
|
||||
refresh_token_details.token_uuid.to_string(),
|
||||
&email,
|
||||
(refresh_token_max_age * 60) as u64,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = refresh_result {
|
||||
error!("Failed to set refresh token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to set refresh token in redis: {}", err),
|
||||
});
|
||||
};
|
||||
|
||||
let access_cookie =
|
||||
Cookie::build("access_token", access_token_details.token.clone().unwrap())
|
||||
.path("/")
|
||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
let refresh_cookie = Cookie::build(
|
||||
"refresh_token",
|
||||
refresh_token_details.token.clone().unwrap(),
|
||||
)
|
||||
.path("/")
|
||||
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
let logged_in_cookie = Cookie::build("logged_in", "true")
|
||||
.path("/")
|
||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||
.http_only(false)
|
||||
.finish();
|
||||
|
||||
let access_token_uuid =
|
||||
uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.cookie(access_cookie)
|
||||
.cookie(refresh_cookie)
|
||||
.cookie(logged_in_cookie)
|
||||
.json(JwtAuth {
|
||||
token: access_token_uuid,
|
||||
user: query_user.into(),
|
||||
})
|
||||
}
|
||||
Err(err) => ResponseError::error_response(&ServiceError {
|
||||
status: 401,
|
||||
message: err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/session")]
|
||||
async fn session(req: HttpRequest) -> HttpResponse {
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
// If there is a access_token cookie, check if it is valid
|
||||
let has_session = match req.cookie("access_token") {
|
||||
Some(cookie) => {
|
||||
let access_token = cookie.value().to_string();
|
||||
let public_key = std::fs::read_to_string(format!("{}access_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
match verify_token(&access_token, &public_key) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
if !has_session {
|
||||
// If there is a refresh_token cookie, check if it is valid
|
||||
match req.cookie("refresh_token") {
|
||||
Some(cookie) => {
|
||||
let refresh_token = cookie.value().to_string();
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
match verify_token(&refresh_token, &public_key) {
|
||||
Ok(_) => return HttpResponse::Ok().json(true),
|
||||
Err(_) => return HttpResponse::Ok().json(false),
|
||||
};
|
||||
}
|
||||
None => return HttpResponse::Ok().json(false),
|
||||
};
|
||||
return HttpResponse::Ok().cookie(session_cookie).finish();
|
||||
} else {
|
||||
return HttpResponse::Ok().json(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RefreshParams {
|
||||
refresh_token_rotation: Option<bool>,
|
||||
}
|
||||
|
||||
#[get("/refresh")]
|
||||
async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
let params = match web::Query::<RefreshParams>::from_query(req.query_string()) {
|
||||
Ok(params) => params,
|
||||
Err(err) => {
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 422,
|
||||
message: err.to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token = match req.cookie("refresh_token") {
|
||||
Some(cookie) => cookie.value().to_string(),
|
||||
None => {
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 401,
|
||||
message: "Refresh token not found".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
|
||||
let email = refresh_token_details.email.clone();
|
||||
|
||||
match QueryUser::get_by_email(&email) {
|
||||
Ok(query_user) => {
|
||||
let access_token_details = match generate_access_token(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate access token: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
let mut conn = match db::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete old auth token if it exists
|
||||
match req.cookie("access_token") {
|
||||
Some(cookie) => {
|
||||
let access_token = cookie.value().to_string();
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||
.expect("Unable to read access public key");
|
||||
match verify_token(&access_token, &public_key) {
|
||||
Ok(token_details) => {
|
||||
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
||||
}
|
||||
Err(_) => {}
|
||||
};
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
|
||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn
|
||||
.set_ex(
|
||||
access_token_details.token_uuid.to_string(),
|
||||
&email,
|
||||
(access_token_max_age * 60) as u64,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = access_result {
|
||||
error!("Failed to set access token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to set access token in redis: {}", err),
|
||||
});
|
||||
};
|
||||
|
||||
let access_cookie =
|
||||
Cookie::build("access_token", access_token_details.token.clone().unwrap())
|
||||
.path("/")
|
||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
let logged_in_cookie = Cookie::build("logged_in", "true")
|
||||
.path("/")
|
||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||
.http_only(false)
|
||||
.finish();
|
||||
|
||||
let access_token_uuid =
|
||||
uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||
|
||||
// Refresh the refresh token if requested
|
||||
let refresh_token_rotation = match params.refresh_token_rotation {
|
||||
Some(refresh_token_rotation) => refresh_token_rotation,
|
||||
None => false,
|
||||
};
|
||||
if refresh_token_rotation {
|
||||
// Delete the old refresh token
|
||||
let _: redis::RedisResult<()> =
|
||||
conn.del(refresh_token_details.token_uuid.to_string()).await;
|
||||
|
||||
let refresh_token_details = match generate_refresh_token(&refresh_token_details.email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate refresh token: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let refresh_result: redis::RedisResult<()> = conn
|
||||
.set_ex(
|
||||
refresh_token_details.token_uuid.to_string(),
|
||||
&refresh_token_details.email,
|
||||
(refresh_token_max_age * 60) as u64,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = refresh_result {
|
||||
error!("Failed to set refresh token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to set refresh token in redis: {}", err),
|
||||
});
|
||||
};
|
||||
|
||||
let refresh_cookie = Cookie::build(
|
||||
"refresh_token",
|
||||
refresh_token_details.token.clone().unwrap(),
|
||||
)
|
||||
.path("/")
|
||||
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.cookie(refresh_cookie)
|
||||
.cookie(access_cookie)
|
||||
.cookie(logged_in_cookie)
|
||||
.json(JwtAuth {
|
||||
token: access_token_uuid,
|
||||
user: query_user.into(),
|
||||
})
|
||||
} else {
|
||||
HttpResponse::Ok()
|
||||
.cookie(access_cookie)
|
||||
.cookie(logged_in_cookie)
|
||||
.json(JwtAuth {
|
||||
token: access_token_uuid,
|
||||
user: query_user.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
log::error!("Invalid login attempt for {}", email);
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
||||
let refresh_token = match req.cookie("refresh_token") {
|
||||
Some(cookie) => cookie.value().to_string(),
|
||||
async fn logout(req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||
// Delete the session from the store
|
||||
match req.cookie(SESSION_COOKIE_NAME) {
|
||||
Some(cookie) => {
|
||||
let session_id = cookie.value().to_string();
|
||||
if let Err(err) = Session::delete(&session_id).await {
|
||||
log::error!("Failed to delete session");
|
||||
return ResponseError::error_response(&ApiError::new(500, err.to_string()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 401,
|
||||
message: "Refresh token not found".to_string(),
|
||||
})
|
||||
return ResponseError::error_response(&ApiError::new(400, "Invalid session".to_string()));
|
||||
}
|
||||
};
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
|
||||
let mut conn = match db::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
};
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn
|
||||
.del(&[
|
||||
refresh_token_details.token_uuid.to_string(),
|
||||
auth.token.to_string(),
|
||||
])
|
||||
.await;
|
||||
if let Err(err) = access_result {
|
||||
error!("Failed to set access token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to set access token in redis: {}", err),
|
||||
});
|
||||
};
|
||||
|
||||
let access_cookie = Cookie::build("access_token", "")
|
||||
let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "")
|
||||
.path("/")
|
||||
.max_age(Duration::new(-1, 0))
|
||||
.http_only(true)
|
||||
.finish();
|
||||
let refresh_cookie = Cookie::build("refresh_token", "")
|
||||
.path("/")
|
||||
.max_age(Duration::new(-1, 0))
|
||||
.http_only(true)
|
||||
.finish();
|
||||
let logged_in_cookie = Cookie::build("logged_in", "")
|
||||
.path("/")
|
||||
.max_age(Duration::new(-1, 0))
|
||||
.max_age(Duration::seconds(-1))
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.cookie(access_cookie)
|
||||
.cookie(refresh_cookie)
|
||||
.cookie(logged_in_cookie)
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[get("/me")]
|
||||
async fn me(auth: JwtAuth) -> HttpResponse {
|
||||
HttpResponse::Ok().json(auth)
|
||||
}
|
||||
|
||||
#[get("/roles")]
|
||||
async fn roles() -> HttpResponse {
|
||||
HttpResponse::Ok().json(Response {
|
||||
data: vec!["admin", "user"],
|
||||
meta: None,
|
||||
})
|
||||
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
let r = RegisterUser {
|
||||
email: "admin".to_string(),
|
||||
password: "admin".to_string(),
|
||||
first_name: "Admin".to_string(),
|
||||
last_name: "Admin".to_string(),
|
||||
};
|
||||
let mut u = r.convert_to_insert().unwrap();
|
||||
u.role = "admin".to_string();
|
||||
u.verified = true;
|
||||
let _ = InsertUser::insert(u);
|
||||
config.service(
|
||||
web::scope("auth")
|
||||
.service(register)
|
||||
.service(login)
|
||||
.service(refresh)
|
||||
.service(logout)
|
||||
.service(me)
|
||||
.service(roles),
|
||||
.service(logout),
|
||||
);
|
||||
}
|
||||
|
||||
91
service/src/auth/session.rs
Normal file
91
service/src/auth/session.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use actix_web::cookie::{time::Duration, Cookie};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
|
||||
use crate::{
|
||||
db::redis_async_connection,
|
||||
error::{ApiError, ApiResult},
|
||||
};
|
||||
|
||||
use super::{csprng_128bit, hash, verify_hash};
|
||||
|
||||
pub const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub session_id: String,
|
||||
pub email: String,
|
||||
pub ip_address: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(email: &str, ip_address: &str) -> Self {
|
||||
let now = chrono::Utc::now();
|
||||
Self {
|
||||
session_id: csprng_128bit(32),
|
||||
email: email.to_string(),
|
||||
ip_address: hash(&ip_address).unwrap(),
|
||||
expires_at: now + chrono::Duration::seconds(DEFAULT_SESSION_TTL),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store(&self) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let key = self.session_id.clone();
|
||||
let value = serde_json::to_string(self)?;
|
||||
let result: RedisResult<()> = conn.set_ex(key, &value, DEFAULT_SESSION_TTL as u64).await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(session_id: &str) -> ApiResult<Option<Self>> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<Option<String>> = conn.get(session_id).await;
|
||||
match result {
|
||||
Ok(Some(value)) => Ok(Some(serde_json::from_str(&value)?)),
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(session_id: &str) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<()> = conn.del(session_id).await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify(session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
// Check if the session exists
|
||||
let session = match Self::get(session_id).await? {
|
||||
Some(session) => session,
|
||||
None => return Err(ApiError::new(401, "Session does not exist".to_string())),
|
||||
};
|
||||
|
||||
// Check if the IP Address matches the Session's IP Address
|
||||
if verify_hash(ip_address, &session.ip_address) {
|
||||
return Ok(session);
|
||||
} else {
|
||||
return Err(ApiError::new(
|
||||
401,
|
||||
"IP Address does not match".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> Cookie {
|
||||
Cookie::build(SESSION_COOKIE_NAME, self.session_id.clone())
|
||||
.path("/")
|
||||
.max_age(Duration::seconds(DEFAULT_SESSION_TTL))
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error_handler::ServiceError;
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use diesel::{r2d2::ConnectionManager, PgConnection};
|
||||
use redis::{Client as RedisClient, aio::MultiplexedConnection as RedisConnection};
|
||||
use s3::{
|
||||
@@ -76,7 +76,7 @@ pub async fn init() {
|
||||
Ok(_) => info!("Bucket initialized"),
|
||||
Err(err) => match err.status {
|
||||
409 => warn!("Bucket already exists"),
|
||||
_ => error!("Failed to initialize bucket; {}", err.message),
|
||||
_ => error!("Failed to initialize bucket; {}", err),
|
||||
},
|
||||
};
|
||||
let mut pool: DbConnection = connection().expect("Failed to get db connection");
|
||||
@@ -86,23 +86,23 @@ pub async fn init() {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn connection() -> Result<DbConnection, ServiceError> {
|
||||
pub fn connection() -> ApiResult<DbConnection> {
|
||||
POOL
|
||||
.get()
|
||||
.map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e)))
|
||||
.map_err(|e| ApiError::new(500, format!("Failed getting db connection: {}", e)))
|
||||
}
|
||||
|
||||
pub fn redis_connection() -> Result<redis::Connection, ServiceError> {
|
||||
pub fn redis_connection() -> ApiResult<redis::Connection> {
|
||||
let conn = REDIS.get_connection()?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn redis_async_connection() -> Result<RedisConnection, ServiceError> {
|
||||
pub async fn redis_async_connection() -> ApiResult<RedisConnection> {
|
||||
let conn = REDIS.get_multiplexed_async_connection().await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
async fn create_bucket() -> Result<CreateBucketResponse, ServiceError> {
|
||||
async fn create_bucket() -> ApiResult<CreateBucketResponse> {
|
||||
let url = env::var("MINIO_URL").unwrap_or("localhost".to_string());
|
||||
let port = env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
||||
let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
||||
@@ -132,18 +132,18 @@ async fn create_bucket() -> Result<CreateBucketResponse, ServiceError> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn upload_file(path: &str, content: &[u8]) -> Result<ResponseData, ServiceError> {
|
||||
pub async fn upload_file(path: &str, content: &[u8]) -> ApiResult<ResponseData> {
|
||||
let response = BUCKET.put_object(path, content).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_file(path: &str) -> Result<Vec<u8>, ServiceError> {
|
||||
pub async fn get_file(path: &str) -> ApiResult<Vec<u8>> {
|
||||
let response = BUCKET.get_object(path).await?;
|
||||
let bytes = response.bytes();
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
pub async fn delete_file(path: &str) -> Result<ResponseData, ServiceError> {
|
||||
pub async fn delete_file(path: &str) -> ApiResult<ResponseData> {
|
||||
let response = BUCKET.delete_object(path).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
154
service/src/error.rs
Normal file
154
service/src/error.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use diesel::result::Error as DieselError;
|
||||
use log::warn;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ApiError {
|
||||
pub status: u16,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn new(status: u16, message: String) -> Self {
|
||||
Self { status, message }
|
||||
}
|
||||
|
||||
pub fn to_http_response(&self) -> HttpResponse {
|
||||
let status = match StatusCode::from_u16(self.status) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
};
|
||||
HttpResponse::build(status).body(self.message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.message.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ApiError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::new(500, format!("Unknown IO error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::env::VarError> for ApiError {
|
||||
fn from(error: std::env::VarError) -> Self {
|
||||
Self::new(
|
||||
500,
|
||||
format!("Unknown environment variable error: {}", error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DieselError> for ApiError {
|
||||
fn from(error: DieselError) -> Self {
|
||||
match error {
|
||||
DieselError::DatabaseError(kind, err) => match kind {
|
||||
diesel::result::DatabaseErrorKind::UniqueViolation => {
|
||||
Self::new(409, err.message().to_string())
|
||||
}
|
||||
_ => Self::new(500, err.message().to_string()),
|
||||
},
|
||||
DieselError::NotFound => Self::new(404, "The record was not found".to_string()),
|
||||
DieselError::SerializationError(err) => Self::new(422, err.to_string()),
|
||||
err => Self::new(500, format!("Unknown Diesel error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ApiError {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Self::new(500, format!("Unknown reqwest error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ApiError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::new(500, format!("Unknown serde_json error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::password_hash::Error> for ApiError {
|
||||
fn from(error: argon2::password_hash::Error) -> Self {
|
||||
Self::new(500, format!("Unknown argon2 error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jsonwebtoken::errors::Error> for ApiError {
|
||||
fn from(error: jsonwebtoken::errors::Error) -> Self {
|
||||
Self::new(500, format!("Unknown jsonwebtoken error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for ApiError {
|
||||
fn from(error: redis::RedisError) -> Self {
|
||||
Self::new(500, format!("Unknown redis error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<s3::error::S3Error> for ApiError {
|
||||
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 ResponseError for ApiError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let status = match StatusCode::from_u16(self.status) {
|
||||
Ok(status) => status,
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let message = match status.as_u16() < 500 {
|
||||
true => self.message.clone(),
|
||||
false => "Internal server error".to_string(),
|
||||
};
|
||||
|
||||
HttpResponse::build(status).json(json!({ "status": status.as_u16(), "message": message }))
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use diesel::result::Error as DieselError;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ServiceError {
|
||||
pub status: u16,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ServiceError {
|
||||
pub fn new(status: u16, message: String) -> ServiceError {
|
||||
ServiceError { status, message }
|
||||
}
|
||||
|
||||
pub fn to_http_response(&self) -> HttpResponse {
|
||||
let status = match StatusCode::from_u16(self.status) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
};
|
||||
HttpResponse::build(status).body(self.message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ServiceError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.message.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ServiceError {
|
||||
fn from(error: std::io::Error) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown IO error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::env::VarError> for ServiceError {
|
||||
fn from(error: std::env::VarError) -> ServiceError {
|
||||
ServiceError::new(
|
||||
500,
|
||||
format!("Unknown environment variable error: {}", error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DieselError> for ServiceError {
|
||||
fn from(error: DieselError) -> ServiceError {
|
||||
match error {
|
||||
DieselError::DatabaseError(kind, err) => match kind {
|
||||
diesel::result::DatabaseErrorKind::UniqueViolation => {
|
||||
ServiceError::new(409, err.message().to_string())
|
||||
}
|
||||
_ => ServiceError::new(500, err.message().to_string()),
|
||||
},
|
||||
DieselError::NotFound => ServiceError::new(404, "The record was not found".to_string()),
|
||||
DieselError::SerializationError(err) => ServiceError::new(422, err.to_string()),
|
||||
err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ServiceError {
|
||||
fn from(error: reqwest::Error) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown reqwest error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ServiceError {
|
||||
fn from(error: serde_json::Error) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown serde_json error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::password_hash::Error> for ServiceError {
|
||||
fn from(error: argon2::password_hash::Error) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown argon2 error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jsonwebtoken::errors::Error> for ServiceError {
|
||||
fn from(error: jsonwebtoken::errors::Error) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown jsonwebtoken error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for ServiceError {
|
||||
fn from(error: redis::RedisError) -> ServiceError {
|
||||
ServiceError::new(500, format!("Unknown redis error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<s3::error::S3Error> for ServiceError {
|
||||
fn from(error: s3::error::S3Error) -> ServiceError {
|
||||
match error {
|
||||
s3::error::S3Error::Credentials(err) => {
|
||||
ServiceError::new(500, format!("Unknown s3 credentials error: {}", err))
|
||||
}
|
||||
s3::error::S3Error::FromUtf8(err) => {
|
||||
ServiceError::new(500, format!("Unknown s3 from utf8 error: {}", err))
|
||||
}
|
||||
s3::error::S3Error::FmtError(err) => {
|
||||
ServiceError::new(500, format!("Unknown s3 fmt error: {}", err))
|
||||
}
|
||||
s3::error::S3Error::HeaderToStr(err) => {
|
||||
ServiceError::new(500, format!("Unknown s3 header to str error: {}", err))
|
||||
}
|
||||
s3::error::S3Error::HmacInvalidLength(err) => ServiceError::new(
|
||||
500,
|
||||
format!("Unknown s3 hmac invalid length error: {}", err),
|
||||
),
|
||||
s3::error::S3Error::Http(error) => ServiceError::new(error.status_code().as_u16(), error.to_string()),
|
||||
_ => ServiceError::new(500, format!("Unknown s3 error: {}", error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for ServiceError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let status = match StatusCode::from_u16(self.status) {
|
||||
Ok(status) => status,
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let message = match status.as_u16() < 500 {
|
||||
true => self.message.clone(),
|
||||
false => "Internal server error".to_string(),
|
||||
};
|
||||
|
||||
HttpResponse::build(status).json(json!({ "status": status.as_u16(), "message": message }))
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,11 @@ use std::env;
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger};
|
||||
use dotenv::dotenv;
|
||||
use log::{info, error};
|
||||
|
||||
mod airports;
|
||||
mod auth;
|
||||
mod db;
|
||||
mod error_handler;
|
||||
mod error;
|
||||
mod metars;
|
||||
mod scheduler;
|
||||
mod users;
|
||||
@@ -45,11 +44,11 @@ async fn main() -> std::io::Result<()> {
|
||||
.bind(format!("{}:{}", host, port))
|
||||
{
|
||||
Ok(b) => {
|
||||
info!("Binding server to {}:{}", host, port);
|
||||
log::info!("Binding server to {}:{}", host, port);
|
||||
b
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not bind server: {}", err);
|
||||
log::error!("Could not bind server: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::airports::QueryAirport;
|
||||
use crate::{error_handler::ServiceError, db};
|
||||
use crate::error::ApiError;
|
||||
use crate::{error::ApiResult, db};
|
||||
use crate::db::schema::metars::{self};
|
||||
use chrono::Datelike;
|
||||
use diesel::{prelude::*, sql_query};
|
||||
@@ -160,7 +161,7 @@ impl Default for Metar {
|
||||
}
|
||||
|
||||
impl Metar {
|
||||
fn parse(metar_strings: Vec<&str>) -> Result<Vec<Self>, ServiceError> {
|
||||
fn parse(metar_strings: Vec<&str>) -> ApiResult<Vec<Self>> {
|
||||
let mut metars: Vec<Self> = vec![];
|
||||
for metar_string in metar_strings {
|
||||
trace!("Parsing METAR data: {}", metar_string);
|
||||
@@ -674,7 +675,7 @@ impl Metar {
|
||||
return missing_metar_icaos;
|
||||
}
|
||||
|
||||
async fn get_remote_metars(icaos: Vec<String>) -> Result<Vec<Metar>, ServiceError> {
|
||||
async fn get_remote_metars(icaos: Vec<String>) -> ApiResult<Vec<Metar>> {
|
||||
let gov_api_url = std::env::var("GOV_API_URL").expect("GOV_API_URL must be set");
|
||||
// Query the remote API for the missing METAR data 10 at a time
|
||||
let icao_chunks = icaos
|
||||
@@ -688,7 +689,7 @@ impl Metar {
|
||||
Ok(r) => {
|
||||
// Check if the status code is 200
|
||||
if r.status() != 200 {
|
||||
return Err(ServiceError::new(
|
||||
return Err(ApiError::new(
|
||||
500,
|
||||
format!("Unable to get METAR request: {}", r.status()),
|
||||
));
|
||||
@@ -706,7 +707,7 @@ impl Metar {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(ServiceError::new(
|
||||
return Err(ApiError::new(
|
||||
500,
|
||||
format!("Unable to parse METAR request: {}", err),
|
||||
))
|
||||
@@ -714,7 +715,7 @@ impl Metar {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(ServiceError::new(
|
||||
return Err(ApiError::new(
|
||||
500,
|
||||
format!("Unable to get METAR request: {}", err),
|
||||
))
|
||||
@@ -749,7 +750,7 @@ impl Metar {
|
||||
return insert_metars;
|
||||
}
|
||||
|
||||
pub async fn get_all(icao_string: String) -> Result<Vec<Self>, ServiceError> {
|
||||
pub async fn get_all(icao_string: String) -> ApiResult<Vec<Self>> {
|
||||
if icao_string.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -834,14 +835,14 @@ struct InsertMetar {
|
||||
}
|
||||
|
||||
impl InsertMetar {
|
||||
fn insert(metars: &Vec<Self>) -> Result<usize, ServiceError> {
|
||||
fn insert(metars: &Vec<Self>) -> ApiResult<usize> {
|
||||
let mut conn = db::connection()?;
|
||||
match diesel::insert_into(metars::table)
|
||||
.values(metars)
|
||||
.execute(&mut conn)
|
||||
{
|
||||
Ok(rows) => Ok(rows),
|
||||
Err(err) => Err(ServiceError {
|
||||
Err(err) => Err(ApiError {
|
||||
status: 500,
|
||||
message: format!("{}", err),
|
||||
}),
|
||||
@@ -860,7 +861,7 @@ struct QueryMetar {
|
||||
}
|
||||
|
||||
impl QueryMetar {
|
||||
fn get_all(icaos: &Vec<&str>) -> Result<Vec<QueryMetar>, ServiceError> {
|
||||
fn get_all(icaos: &Vec<&str>) -> ApiResult<Vec<QueryMetar>> {
|
||||
// Sanitize search to only allow [a-zA-Z0-9]
|
||||
let icaos = icaos
|
||||
.iter()
|
||||
@@ -880,7 +881,7 @@ impl QueryMetar {
|
||||
format!("SELECT DISTINCT ON (icao) * FROM metars WHERE icao IN ({}) ORDER BY icao, observation_time DESC", station_query.join(","))
|
||||
).load(&mut conn) {
|
||||
Ok(m) => m,
|
||||
Err(err) => return Err(ServiceError { status: 500, message: format!("{}", err) })
|
||||
Err(err) => return Err(ApiError { status: 500, message: format!("{}", err) })
|
||||
};
|
||||
return Ok(db_metars);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{error_handler::ServiceError, db::Metadata};
|
||||
use crate::{error::ApiError, db::Metadata};
|
||||
use crate::metars::Metar;
|
||||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
||||
use log::error;
|
||||
@@ -25,7 +25,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
|
||||
};
|
||||
|
||||
let metars =
|
||||
match web::block(|| Ok::<_, ServiceError>(async { Metar::get_all(icao_string).await }))
|
||||
match web::block(|| Ok::<_, ApiError>(async { Metar::get_all(icao_string).await }))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::init_routes;
|
||||
|
||||
108
service/src/users/model.rs
Normal file
108
service/src/users/model.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{
|
||||
auth::hash,
|
||||
db::{connection, schema::users},
|
||||
error::ApiResult,
|
||||
};
|
||||
|
||||
/**
|
||||
* RegisterRequest
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
impl RegisterRequest {
|
||||
pub fn to_user(self) -> ApiResult<User> {
|
||||
let hash = hash(&self.password)?;
|
||||
Ok(User {
|
||||
email: self.email.to_lowercase(),
|
||||
hash,
|
||||
role: "user".to_string(),
|
||||
first_name: self.first_name,
|
||||
last_name: self.last_name,
|
||||
updated_at: chrono::Utc::now().naive_utc(),
|
||||
created_at: chrono::Utc::now().naive_utc(),
|
||||
profile_picture: None,
|
||||
favorites: vec![],
|
||||
verified: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginRequest
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/**
|
||||
* UserResponse
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserResponse {
|
||||
pub email: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub profile_picture: Option<String>,
|
||||
}
|
||||
|
||||
impl From<User> for UserResponse {
|
||||
fn from(user: User) -> Self {
|
||||
UserResponse {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
profile_picture: user.profile_picture,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User
|
||||
*/
|
||||
#[derive(Debug, Insertable, AsChangeset, Queryable, QueryableByName, Serialize, Deserialize)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct User {
|
||||
pub email: String,
|
||||
pub hash: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub profile_picture: Option<String>,
|
||||
pub favorites: Vec<String>,
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn get_by_email(email: &str) -> ApiResult<User> {
|
||||
let mut conn = connection()?;
|
||||
// Check if the user exists by email, case insensitive
|
||||
|
||||
let user = users::table
|
||||
.filter(users::email.eq(email.to_lowercase()))
|
||||
.first(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub fn insert(user: Self) -> ApiResult<User> {
|
||||
let mut conn = connection()?;
|
||||
let user = diesel::insert_into(users::table)
|
||||
.values(user)
|
||||
.get_result(&mut conn)?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
@@ -1,164 +1,165 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, post, delete, web, HttpResponse, ResponseError};
|
||||
use futures_util::StreamExt;
|
||||
// use actix_multipart::Multipart;
|
||||
// use actix_web::{get, post, delete, web, HttpResponse, ResponseError};
|
||||
// use futures_util::StreamExt;
|
||||
|
||||
use crate::{
|
||||
auth::{JwtAuth, QueryUser, InsertUser},
|
||||
error_handler::ServiceError,
|
||||
db::{upload_file, get_file, delete_file},
|
||||
};
|
||||
// use crate::{
|
||||
// auth::Auth,
|
||||
// db::{delete_file, get_file, upload_file},
|
||||
// error::ServiceError,
|
||||
// users::User,
|
||||
// };
|
||||
|
||||
#[get("/favorites")]
|
||||
async fn get_favorites(auth: JwtAuth) -> HttpResponse {
|
||||
match QueryUser::get_by_email(&auth.user.email) {
|
||||
Ok(user) => return HttpResponse::Ok().json(user.favorites),
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/favorites/{icao}")]
|
||||
async fn add_favorite(icao: web::Path<String>, auth: JwtAuth) -> HttpResponse {
|
||||
match QueryUser::get_by_email(&auth.user.email) {
|
||||
Ok(user) => {
|
||||
if user.favorites.contains(&icao) {
|
||||
// Check if the airport ICAO is already in the user's favorites
|
||||
return HttpResponse::Conflict().finish();
|
||||
} else {
|
||||
// Add the airport ICAO to the user's favorites
|
||||
let mut favorites = user.favorites;
|
||||
favorites.push(icao.into_inner());
|
||||
match InsertUser::update_favorites(&user.email, favorites) {
|
||||
Ok(_) => return HttpResponse::Ok().finish(),
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/favorites/{icao}")]
|
||||
async fn delete_favorite(icao: web::Path<String>, auth: JwtAuth) -> HttpResponse {
|
||||
let icao: String = icao.into_inner();
|
||||
match QueryUser::get_by_email(&auth.user.email) {
|
||||
Ok(user) => {
|
||||
if user.favorites.contains(&icao) {
|
||||
// Check if the airport ICAO is already in the user's favorites
|
||||
let mut favorites = user.favorites;
|
||||
favorites.retain(|x| x != &icao);
|
||||
match InsertUser::update_favorites(&user.email, favorites) {
|
||||
Ok(_) => return HttpResponse::Ok().finish(),
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
}
|
||||
} else {
|
||||
// Remove the airport ICAO from the user's favorites
|
||||
return HttpResponse::Conflict().finish();
|
||||
}
|
||||
}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/picture")]
|
||||
async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut bytes = web::BytesMut::new();
|
||||
let mut field = match item {
|
||||
Ok(field) => field,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
let content_type = field.content_disposition();
|
||||
let filename = match content_type.unwrap().get_filename() {
|
||||
Some(name) => match name.split(".").last() {
|
||||
Some(ext) => match ext {
|
||||
"apng" | "avif" | "gif" | "jpg" | "jpeg" | "jfif" | "pjpeg" | "pjp" | "png" | "svg"
|
||||
| "webp" => name,
|
||||
_ => {
|
||||
return ResponseError::error_response(&ServiceError::new(
|
||||
400,
|
||||
"File extension is not supported".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return ResponseError::error_response(&ServiceError::new(
|
||||
400,
|
||||
"Unknown file extension".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return ResponseError::error_response(&ServiceError::new(
|
||||
400,
|
||||
"File name is not provided".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let path = format!("users/{}/{}", auth.user.email, filename);
|
||||
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = match chunk {
|
||||
Ok(data) => data,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
bytes.extend_from_slice(&data);
|
||||
}
|
||||
match upload_file(&path, &bytes).await {
|
||||
Ok(_) => {
|
||||
match InsertUser::update_profile_picture(&auth.user.email, Some(&path)) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
}
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
#[get("/picture")]
|
||||
async fn get_picture(auth: JwtAuth) -> HttpResponse {
|
||||
let user = match QueryUser::get_by_email(&auth.user.email) {
|
||||
Ok(user) => user,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
if let Some(path) = user.profile_picture {
|
||||
match get_file(&path).await {
|
||||
Ok(bytes) => HttpResponse::Ok().body(bytes),
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/picture")]
|
||||
async fn delete_picture(auth: JwtAuth) -> HttpResponse {
|
||||
let user = match QueryUser::get_by_email(&auth.user.email) {
|
||||
Ok(user) => user,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
if let Some(path) = user.profile_picture {
|
||||
match delete_file(&path).await {
|
||||
Ok(_) => match InsertUser::update_profile_picture(&auth.user.email, None) {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
},
|
||||
Err(err) => ResponseError::error_response(&err),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("users")
|
||||
.service(get_favorites)
|
||||
.service(add_favorite)
|
||||
.service(delete_favorite)
|
||||
.service(set_picture)
|
||||
.service(get_picture)
|
||||
.service(delete_picture),
|
||||
);
|
||||
// #[get("/favorites")]
|
||||
// async fn get_favorites(auth: Auth) -> HttpResponse {
|
||||
// match User::get_by_email(&auth.user.email) {
|
||||
// Ok(user) => return HttpResponse::Ok().json(user.favorites),
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[post("/favorites/{icao}")]
|
||||
// async fn add_favorite(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
// match User::get_by_email(&auth.user.email) {
|
||||
// Ok(user) => {
|
||||
// if user.favorites.contains(&icao) {
|
||||
// // Check if the airport ICAO is already in the user's favorites
|
||||
// return HttpResponse::Conflict().finish();
|
||||
// } else {
|
||||
// // Add the airport ICAO to the user's favorites
|
||||
// let mut favorites = user.favorites;
|
||||
// favorites.push(icao.into_inner());
|
||||
// match User::update_favorites(&user.email, favorites) {
|
||||
// Ok(_) => return HttpResponse::Ok().finish(),
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[delete("/favorites/{icao}")]
|
||||
// async fn delete_favorite(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
// let icao: String = icao.into_inner();
|
||||
// match User::get_by_email(&auth.user.email) {
|
||||
// Ok(user) => {
|
||||
// if user.favorites.contains(&icao) {
|
||||
// // Check if the airport ICAO is already in the user's favorites
|
||||
// let mut favorites = user.favorites;
|
||||
// favorites.retain(|x| x != &icao);
|
||||
// match User::update_favorites(&user.email, favorites) {
|
||||
// Ok(_) => return HttpResponse::Ok().finish(),
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// }
|
||||
// } else {
|
||||
// // Remove the airport ICAO from the user's favorites
|
||||
// return HttpResponse::Conflict().finish();
|
||||
// }
|
||||
// }
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[post("/picture")]
|
||||
// async fn set_picture(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
// while let Some(item) = payload.next().await {
|
||||
// let mut bytes = web::BytesMut::new();
|
||||
// let mut field = match item {
|
||||
// Ok(field) => field,
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// let content_type = field.content_disposition();
|
||||
// let filename = match content_type.unwrap().get_filename() {
|
||||
// Some(name) => match name.split(".").last() {
|
||||
// Some(ext) => match ext {
|
||||
// "apng" | "avif" | "gif" | "jpg" | "jpeg" | "jfif" | "pjpeg" | "pjp" | "png" | "svg"
|
||||
// | "webp" => name,
|
||||
// _ => {
|
||||
// return ResponseError::error_response(&ServiceError::new(
|
||||
// 400,
|
||||
// "File extension is not supported".to_string(),
|
||||
// ))
|
||||
// }
|
||||
// },
|
||||
// None => {
|
||||
// return ResponseError::error_response(&ServiceError::new(
|
||||
// 400,
|
||||
// "Unknown file extension".to_string(),
|
||||
// ))
|
||||
// }
|
||||
// },
|
||||
// None => {
|
||||
// return ResponseError::error_response(&ServiceError::new(
|
||||
// 400,
|
||||
// "File name is not provided".to_string(),
|
||||
// ))
|
||||
// }
|
||||
// };
|
||||
// let path = format!("users/{}/{}", auth.user.email, filename);
|
||||
|
||||
// while let Some(chunk) = field.next().await {
|
||||
// let data = match chunk {
|
||||
// Ok(data) => data,
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// bytes.extend_from_slice(&data);
|
||||
// }
|
||||
// match upload_file(&path, &bytes).await {
|
||||
// Ok(_) => {
|
||||
// match User::update_profile_picture(&auth.user.email, Some(&path)) {
|
||||
// Ok(_) => {}
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// }
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// }
|
||||
// HttpResponse::Ok().finish()
|
||||
// }
|
||||
|
||||
// #[get("/picture")]
|
||||
// async fn get_picture(auth: Auth) -> HttpResponse {
|
||||
// let user = match User::get_by_email(&auth.user.email) {
|
||||
// Ok(user) => user,
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// if let Some(path) = user.profile_picture {
|
||||
// match get_file(&path).await {
|
||||
// Ok(bytes) => HttpResponse::Ok().body(bytes),
|
||||
// Err(err) => ResponseError::error_response(&err),
|
||||
// }
|
||||
// } else {
|
||||
// HttpResponse::NotFound().finish()
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[delete("/picture")]
|
||||
// async fn delete_picture(auth: Auth) -> HttpResponse {
|
||||
// let user = match User::get_by_email(&auth.user.email) {
|
||||
// Ok(user) => user,
|
||||
// Err(err) => return ResponseError::error_response(&err),
|
||||
// };
|
||||
// if let Some(path) = user.profile_picture {
|
||||
// match delete_file(&path).await {
|
||||
// Ok(_) => match User::update_profile_picture(&auth.user.email, None) {
|
||||
// Ok(_) => HttpResponse::Ok().finish(),
|
||||
// Err(err) => ResponseError::error_response(&err),
|
||||
// },
|
||||
// Err(err) => ResponseError::error_response(&err),
|
||||
// }
|
||||
// } else {
|
||||
// HttpResponse::NotFound().finish()
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn init_routes(config: &mut actix_web::web::ServiceConfig) {
|
||||
// config.service(
|
||||
// web::scope("users")
|
||||
// .service(get_favorites)
|
||||
// .service(add_favorite)
|
||||
// .service(delete_favorite)
|
||||
// .service(set_picture)
|
||||
// .service(get_picture)
|
||||
// .service(delete_picture),
|
||||
// );
|
||||
}
|
||||
|
||||
5
ui/.env
5
ui/.env
@@ -1,5 +0,0 @@
|
||||
SERVICE_HOST=service
|
||||
SERVICE_PORT=5000
|
||||
|
||||
UI_PORT=3000
|
||||
NODE_ENV=development
|
||||
@@ -1,26 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
name: aviation
|
||||
services:
|
||||
ui:
|
||||
container_name: aviation-ui
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
ports:
|
||||
- ${UI_PORT:-3000}:3000
|
||||
build:
|
||||
context: ./
|
||||
target: dev
|
||||
command: "npm run dev"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./public:/app/public
|
||||
- ./styles:/app/styles
|
||||
networks:
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
Reference in New Issue
Block a user