Migrate away from .env, use a standard config file instead
This commit is contained in:
parent
cac66699d1
commit
1f3ce078e1
18 changed files with 308 additions and 330 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -128,3 +128,5 @@ fabric.properties
|
||||||
# Android studio 3.1+ serialized cache file
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# API specific ignores
|
||||||
|
api/debt-pirate.config.toml
|
||||||
|
|
9
api/.env
9
api/.env
|
@ -1,9 +0,0 @@
|
||||||
ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets
|
|
||||||
CACHE_URL=redis://debt_pirate:H553jOui2734@192.168.122.251:6379
|
|
||||||
DATABASE_URL=postgres://debt_pirate:HRURqlUmtjIy@192.168.122.251/debt_pirate
|
|
||||||
HOSTNAME=127.0.0.1
|
|
||||||
MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy
|
|
||||||
PORT=42069
|
|
||||||
RUST_LOG=debt_pirate=trace
|
|
||||||
SEND_VERIFICATION_EMAIL=true
|
|
||||||
TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q
|
|
|
@ -1,3 +0,0 @@
|
||||||
TOKEN_KEY=
|
|
||||||
DB_CONNECTION_URI=
|
|
||||||
ASSETS_DIR=
|
|
|
@ -5,15 +5,14 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
axum = { version = "0.7", features = [
|
axum = { version = "0.8", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"multipart",
|
"multipart",
|
||||||
"ws",
|
"ws",
|
||||||
] }
|
] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
bb8-redis = "0.17"
|
bb8-redis = "0.20"
|
||||||
blake3 = { version = "1.5", features = ["serde"] }
|
blake3 = { version = "1.5", features = ["serde"] }
|
||||||
dotenvy = "0.15"
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http = "1.0"
|
http = "1.0"
|
||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
|
@ -29,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
pasetors = "0.7"
|
pasetors = "0.7"
|
||||||
redis = { version = "0.27", features = ["aio"] }
|
redis = { version = "0.28", features = ["aio"] }
|
||||||
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_with = "3.9"
|
serde_with = "3.9"
|
||||||
|
@ -42,6 +41,7 @@ sqlx = { version = "0.8", features = [
|
||||||
syslog-tracing = "0.3.1"
|
syslog-tracing = "0.3.1"
|
||||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||||
tokio = { version = "1.35", features = ["full"] }
|
tokio = { version = "1.35", features = ["full"] }
|
||||||
|
toml = "0.8"
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
tower-http = { version = "0.6", features = ["full"] }
|
tower-http = { version = "0.6", features = ["full"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
|
23
api/debt-pirate.config.example.toml
Normal file
23
api/debt-pirate.config.example.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
hostname = "127.0.0.1"
|
||||||
|
port = "42069"
|
||||||
|
assets_dir = ''
|
||||||
|
log = "info"
|
||||||
|
|
||||||
|
[secrets]
|
||||||
|
token_key = ''
|
||||||
|
|
||||||
|
[database]
|
||||||
|
host = ""
|
||||||
|
port = "5432"
|
||||||
|
user = "debt_pirate"
|
||||||
|
password = ""
|
||||||
|
schema = "debt_pirate"
|
||||||
|
|
||||||
|
[cache]
|
||||||
|
host = "192.168.122.76"
|
||||||
|
port = "6379"
|
||||||
|
user = "debt_pirate"
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
[mail]
|
||||||
|
send_verification_email = true
|
|
@ -1,7 +1,5 @@
|
||||||
use std::{process, sync::mpsc::channel};
|
use std::{process, sync::mpsc::channel};
|
||||||
|
|
||||||
use models::Environment;
|
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
mod models;
|
mod models;
|
||||||
mod requests;
|
mod requests;
|
||||||
|
@ -17,29 +15,20 @@ use tracing::{error, info};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let (tx, rx) = channel::<UserConfirmationMessage>();
|
let config = match services::config::read_config() {
|
||||||
|
Ok(config) => config,
|
||||||
let env = match Environment::init(tx) {
|
|
||||||
Ok(env) => env,
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initialize_logger(&env);
|
initialize_logger(config.logging_directive());
|
||||||
|
|
||||||
info!("Initializing database connection pool...");
|
info!("Initializing database connection pool...");
|
||||||
let db_pool = create_db_connection_pool(env.db_connection_uri().to_string()).await;
|
let db_pool = create_db_connection_pool(config.db_connection_uri().to_string()).await;
|
||||||
info!("Database connection pool created successfully.");
|
info!("Database connection pool created successfully.");
|
||||||
|
|
||||||
info!("Initializing cache service connection pool...");
|
|
||||||
let cache_pool = create_cache_connection_pool(env.cache_url().to_string())
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| error!(?err))
|
|
||||||
.unwrap();
|
|
||||||
info!("Cache service connection pool created successfully.");
|
|
||||||
|
|
||||||
info!("Running database schema migrations...");
|
info!("Running database schema migrations...");
|
||||||
if let Err(err) = run_migrations(&db_pool).await {
|
if let Err(err) = run_migrations(&db_pool).await {
|
||||||
eprintln!("{err:?}");
|
eprintln!("{err:?}");
|
||||||
|
@ -47,11 +36,19 @@ async fn main() {
|
||||||
}
|
}
|
||||||
info!("Database schema migrations completed successfully.");
|
info!("Database schema migrations completed successfully.");
|
||||||
|
|
||||||
|
info!("Initializing cache service connection pool...");
|
||||||
|
let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string())
|
||||||
|
.await
|
||||||
|
.inspect_err(|err| error!(?err))
|
||||||
|
.unwrap();
|
||||||
|
info!("Cache service connection pool created successfully.");
|
||||||
|
|
||||||
info!("Starting email sender service...");
|
info!("Starting email sender service...");
|
||||||
start_emailer_service(Handle::current(), env.assets_dir(), rx);
|
let (tx, rx) = channel::<UserConfirmationMessage>();
|
||||||
|
start_emailer_service(Handle::current(), config.assets_dir(), rx);
|
||||||
info!("Email service started successfully.");
|
info!("Email service started successfully.");
|
||||||
|
|
||||||
if let Err(err) = start_app(db_pool, cache_pool, env).await {
|
if let Err(err) = start_app(db_pool, cache_pool, config, tx).await {
|
||||||
eprintln!("{err:?}");
|
eprintln!("{err:?}");
|
||||||
process::exit(3);
|
process::exit(3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,238 +0,0 @@
|
||||||
use std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::mpsc::Sender,
|
|
||||||
};
|
|
||||||
|
|
||||||
use pasetors::{keys::SymmetricKey, version4::V4};
|
|
||||||
use tracing::trace;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::services::UserConfirmationMessage;
|
|
||||||
|
|
||||||
use super::AppError;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Environment {
|
|
||||||
assets_dir: PathBuf,
|
|
||||||
cache_url: Url,
|
|
||||||
database_url: Url,
|
|
||||||
email_sender: Sender<UserConfirmationMessage>,
|
|
||||||
hostname: String,
|
|
||||||
port: u32,
|
|
||||||
rust_log: String,
|
|
||||||
send_verification_email: bool,
|
|
||||||
token_key: SymmetricKey<V4>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Environment {
|
|
||||||
pub fn init(email_sender: Sender<UserConfirmationMessage>) -> Result<Self, AppError> {
|
|
||||||
let mut builder = EnvironmentObjectBuilder::new(email_sender);
|
|
||||||
dotenvy::dotenv_iter()
|
|
||||||
.expect("Missing .env file")
|
|
||||||
.filter_map(|item| item.ok())
|
|
||||||
.for_each(|(key, value)| match key.as_str() {
|
|
||||||
"ASSETS_DIR" => builder.with_assets_dir(value),
|
|
||||||
"CACHE_URL" => builder.with_cache_url(value),
|
|
||||||
"DATABASE_URL" => builder.with_database_url(value),
|
|
||||||
"HOSTNAME" => builder.with_hostname(value),
|
|
||||||
"PORT" => builder.with_port(value),
|
|
||||||
"RUST_LOG" => builder.with_rust_log(value),
|
|
||||||
"SEND_VERIFICATION_EMAIL" => builder.with_send_verification_email(value),
|
|
||||||
"TOKEN_KEY" => builder.with_token_key(value),
|
|
||||||
_ => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
let missing_vars = builder.uninitialized_variables();
|
|
||||||
if let Some(missing_vars) = missing_vars {
|
|
||||||
Err(AppError::missing_environment_variables(missing_vars))
|
|
||||||
} else {
|
|
||||||
Ok(Environment::from(builder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assets_dir(&self) -> &Path {
|
|
||||||
self.assets_dir.as_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_url(&self) -> &Url {
|
|
||||||
&self.cache_url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn db_connection_uri(&self) -> &Url {
|
|
||||||
&self.database_url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn email_sender(&self) -> &Sender<UserConfirmationMessage> {
|
|
||||||
&self.email_sender
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hostname(&self) -> &str {
|
|
||||||
self.hostname.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn port(&self) -> u32 {
|
|
||||||
self.port
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rust_log(&self) -> &str {
|
|
||||||
self.rust_log.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_verification_email(&self) -> bool {
|
|
||||||
self.send_verification_email
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn token_key(&self) -> &SymmetricKey<V4> {
|
|
||||||
&self.token_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<EnvironmentObjectBuilder> for Environment {
|
|
||||||
fn from(builder: EnvironmentObjectBuilder) -> Self {
|
|
||||||
let EnvironmentObjectBuilder {
|
|
||||||
assets_dir,
|
|
||||||
cache_url,
|
|
||||||
database_url,
|
|
||||||
email_sender,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
rust_log,
|
|
||||||
send_verification,
|
|
||||||
token_key,
|
|
||||||
} = builder;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
assets_dir: assets_dir.unwrap(),
|
|
||||||
cache_url: cache_url.unwrap(),
|
|
||||||
database_url: database_url.unwrap(),
|
|
||||||
email_sender: email_sender.unwrap(),
|
|
||||||
hostname: hostname.unwrap(),
|
|
||||||
port: port.unwrap(),
|
|
||||||
rust_log: rust_log.unwrap(),
|
|
||||||
send_verification_email: send_verification.unwrap_or(true),
|
|
||||||
token_key: token_key.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct EnvironmentObjectBuilder {
|
|
||||||
pub assets_dir: Option<PathBuf>,
|
|
||||||
pub cache_url: Option<Url>,
|
|
||||||
pub database_url: Option<Url>,
|
|
||||||
pub email_sender: Option<Sender<UserConfirmationMessage>>,
|
|
||||||
pub hostname: Option<String>,
|
|
||||||
pub port: Option<u32>,
|
|
||||||
pub rust_log: Option<String>,
|
|
||||||
pub send_verification: Option<bool>,
|
|
||||||
pub token_key: Option<SymmetricKey<V4>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EnvironmentObjectBuilder {
|
|
||||||
pub fn new(email_sender: Sender<UserConfirmationMessage>) -> Self {
|
|
||||||
Self {
|
|
||||||
email_sender: Some(email_sender),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uninitialized_variables(&self) -> Option<Vec<&'static str>> {
|
|
||||||
let mut missing_vars = [
|
|
||||||
("HOSTNAME", self.hostname.as_deref()),
|
|
||||||
("RUST_LOG", self.rust_log.as_deref()),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(key, value)| value.map(|_| key).xor(Some(key)))
|
|
||||||
.collect::<Vec<&'static str>>();
|
|
||||||
|
|
||||||
if self.cache_url.is_none() {
|
|
||||||
missing_vars.push("CACHE_URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.database_url.is_none() {
|
|
||||||
missing_vars.push("DATABASE_URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.token_key.is_none() {
|
|
||||||
missing_vars.push("TOKEN_KEY");
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.assets_dir.is_none() {
|
|
||||||
missing_vars.push("ASSETS_DIR");
|
|
||||||
}
|
|
||||||
|
|
||||||
if missing_vars.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(missing_vars)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_hostname(&mut self, hostname: String) {
|
|
||||||
self.hostname = Some(hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_port(&mut self, port: String) {
|
|
||||||
let port = port
|
|
||||||
.parse::<u32>()
|
|
||||||
.inspect_err(|err| eprintln!("Not a valid port, defaulting to '42069': {err}"))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
self.port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_token_key(&mut self, key: String) {
|
|
||||||
match SymmetricKey::<V4>::try_from(key.as_str()).map_err(|_| AppError::token_key()) {
|
|
||||||
Ok(key) => self.token_key = Some(key),
|
|
||||||
Err(err) => panic!("{err}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cache_url(&mut self, url: String) {
|
|
||||||
trace!(?url);
|
|
||||||
|
|
||||||
let cache_url = url
|
|
||||||
.parse::<Url>()
|
|
||||||
.expect("The 'CACHE_URL' variable is not in valid URI format");
|
|
||||||
|
|
||||||
trace!(?cache_url);
|
|
||||||
|
|
||||||
if cache_url.scheme().to_lowercase() != "redis" {
|
|
||||||
panic!("The 'CACHE_URL' must be a valid Redis connection string; it must use the 'redis://' scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cache_url = Some(cache_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_database_url(&mut self, url: String) {
|
|
||||||
let database_url = url
|
|
||||||
.parse::<Url>()
|
|
||||||
.expect("The 'DATABASE_URL' variable is not in valid URI format");
|
|
||||||
|
|
||||||
self.database_url = Some(database_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_assets_dir(&mut self, assets_dir_path: String) {
|
|
||||||
let assets_dir = PathBuf::from(assets_dir_path);
|
|
||||||
if !assets_dir.try_exists().unwrap_or_default() {
|
|
||||||
panic!(
|
|
||||||
"The 'ASSETS_DIR' environment variable points to a directory that doesn't exist."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if assets_dir.is_file() {
|
|
||||||
panic!("The 'ASSETS_DIR' environment variable must be set to a directory, not a file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assets_dir = Some(assets_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_rust_log(&mut self, rust_log: String) {
|
|
||||||
self.rust_log = Some(rust_log);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_send_verification_email(&mut self, send_verification_email: String) {
|
|
||||||
let send_verification_email = send_verification_email.to_lowercase() == "true";
|
|
||||||
self.send_verification = Some(send_verification_email);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,16 @@
|
||||||
use std::{borrow::Cow, error::Error, fmt::Display, io};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
error::Error,
|
||||||
|
fmt::Display,
|
||||||
|
io::{self, Error as IoError},
|
||||||
|
};
|
||||||
|
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use bb8_redis::bb8::RunError;
|
use bb8_redis::bb8::RunError;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use redis::RedisError;
|
use redis::RedisError;
|
||||||
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
|
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
|
||||||
|
use toml::{self, de::Error as TomlError};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use super::ApiResponse;
|
use super::ApiResponse;
|
||||||
|
@ -49,8 +55,8 @@ impl AppError {
|
||||||
Self::new(ErrorKind::MissingAuthorizationToken)
|
Self::new(ErrorKind::MissingAuthorizationToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
pub fn missing_config_file() -> Self {
|
||||||
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
Self::new(ErrorKind::MissingConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn missing_session_field(field: &'static str) -> Self {
|
pub fn missing_session_field(field: &'static str) -> Self {
|
||||||
|
@ -65,10 +71,6 @@ impl AppError {
|
||||||
Self::new(ErrorKind::NoUserFound)
|
Self::new(ErrorKind::NoUserFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn token_key() -> Self {
|
|
||||||
Self::new(ErrorKind::TokenKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_duplicate_record(&self) -> bool {
|
pub fn is_duplicate_record(&self) -> bool {
|
||||||
matches!(self.kind, ErrorKind::DuplicateRecord(_))
|
matches!(self.kind, ErrorKind::DuplicateRecord(_))
|
||||||
}
|
}
|
||||||
|
@ -84,12 +86,32 @@ impl From<ErrorKind> for AppError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<IoError> for AppError {
|
||||||
|
fn from(other: IoError) -> Self {
|
||||||
|
Self::new(ErrorKind::ConfigFile(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<MigrateError> for AppError {
|
impl From<MigrateError> for AppError {
|
||||||
fn from(other: MigrateError) -> Self {
|
fn from(other: MigrateError) -> Self {
|
||||||
Self::new(ErrorKind::DbMigration(other))
|
Self::new(ErrorKind::DbMigration(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RedisError> for AppError {
|
||||||
|
fn from(other: RedisError) -> Self {
|
||||||
|
trace!(err = ?other, "Cache error");
|
||||||
|
Self::new(ErrorKind::Cache(other.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RunError<RedisError>> for AppError {
|
||||||
|
fn from(other: RunError<RedisError>) -> Self {
|
||||||
|
trace!(err = ?other, "Cache pool error");
|
||||||
|
Self::new(ErrorKind::Cache(other.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SqlxError> for AppError {
|
impl From<SqlxError> for AppError {
|
||||||
fn from(other: SqlxError) -> Self {
|
fn from(other: SqlxError) -> Self {
|
||||||
match &other {
|
match &other {
|
||||||
|
@ -107,17 +129,9 @@ impl From<SqlxError> for AppError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RedisError> for AppError {
|
impl From<TomlError> for AppError {
|
||||||
fn from(other: RedisError) -> Self {
|
fn from(other: TomlError) -> Self {
|
||||||
trace!(err = ?other, "Cache error");
|
Self::new(ErrorKind::ConfigParse(other))
|
||||||
Self::new(ErrorKind::Cache(other.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RunError<RedisError>> for AppError {
|
|
||||||
fn from(other: RunError<RedisError>) -> Self {
|
|
||||||
trace!(err = ?other, "Cache pool error");
|
|
||||||
Self::new(ErrorKind::Cache(other.to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +140,10 @@ impl Display for AppError {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
ErrorKind::AppStartupError(err) => write!(f, "{err}"),
|
ErrorKind::AppStartupError(err) => write!(f, "{err}"),
|
||||||
ErrorKind::Cache(err) => write!(f, "{err}"),
|
ErrorKind::Cache(err) => write!(f, "{err}"),
|
||||||
|
ErrorKind::ConfigFile(err) => write!(f, "Unable to application config file: {err}"),
|
||||||
|
ErrorKind::ConfigParse(err) => {
|
||||||
|
write!(f, "Unable to parse application config file: {err}")
|
||||||
|
}
|
||||||
ErrorKind::ConnectionInfo(service) => write!(
|
ErrorKind::ConnectionInfo(service) => write!(
|
||||||
f,
|
f,
|
||||||
"Unable to connect to '{service}' service; invalid connection string"
|
"Unable to connect to '{service}' service; invalid connection string"
|
||||||
|
@ -141,12 +159,8 @@ impl Display for AppError {
|
||||||
ErrorKind::ExpiredToken => write!(f, "The provided token has expired"),
|
ErrorKind::ExpiredToken => write!(f, "The provided token has expired"),
|
||||||
ErrorKind::InvalidCredentials => write!(f, "Invalid email address or password"),
|
ErrorKind::InvalidCredentials => write!(f, "Invalid email address or password"),
|
||||||
ErrorKind::InvalidToken => write!(f, "The provided token is invalid"),
|
ErrorKind::InvalidToken => write!(f, "The provided token is invalid"),
|
||||||
|
ErrorKind::MissingConfigFile => write!(f, "Application config file not found"),
|
||||||
ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"),
|
ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"),
|
||||||
ErrorKind::MissingEnvironmentVariables(missing_vars) => write!(
|
|
||||||
f,
|
|
||||||
"Missing required environment variables: {}",
|
|
||||||
missing_vars.join(", ")
|
|
||||||
),
|
|
||||||
ErrorKind::MissingSessionField(field) => write!(
|
ErrorKind::MissingSessionField(field) => write!(
|
||||||
f,
|
f,
|
||||||
"Cannot retrieve session: missing required field: {field}"
|
"Cannot retrieve session: missing required field: {field}"
|
||||||
|
@ -168,6 +182,8 @@ impl Display for AppError {
|
||||||
enum ErrorKind {
|
enum ErrorKind {
|
||||||
AppStartupError(io::Error),
|
AppStartupError(io::Error),
|
||||||
Cache(String),
|
Cache(String),
|
||||||
|
ConfigFile(IoError),
|
||||||
|
ConfigParse(TomlError),
|
||||||
ConnectionInfo(&'static str),
|
ConnectionInfo(&'static str),
|
||||||
Database,
|
Database,
|
||||||
DbMigration(MigrateError),
|
DbMigration(MigrateError),
|
||||||
|
@ -175,7 +191,7 @@ enum ErrorKind {
|
||||||
ExpiredToken,
|
ExpiredToken,
|
||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
MissingEnvironmentVariables(Vec<&'static str>),
|
MissingConfigFile,
|
||||||
MissingSessionField(&'static str),
|
MissingSessionField(&'static str),
|
||||||
MissingAuthorizationToken,
|
MissingAuthorizationToken,
|
||||||
NoDbRecordFound,
|
NoDbRecordFound,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
mod api_response;
|
mod api_response;
|
||||||
mod environment;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
pub use api_response::*;
|
pub use api_response::*;
|
||||||
pub use environment::*;
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use session::*;
|
pub use session::*;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use axum::{async_trait, extract::FromRequestParts};
|
use axum::extract::FromRequestParts;
|
||||||
use http::request::Parts;
|
use http::request::Parts;
|
||||||
use humantime::format_rfc3339;
|
use humantime::format_rfc3339;
|
||||||
use redis::ToRedisArgs;
|
use redis::ToRedisArgs;
|
||||||
|
@ -41,7 +41,6 @@ impl Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl FromRequestParts<AppState> for Session {
|
impl FromRequestParts<AppState> for Session {
|
||||||
type Rejection = AppError;
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ impl FromRequestParts<AppState> for Session {
|
||||||
state: &'b AppState,
|
state: &'b AppState,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
let cache_pool = state.cache_pool();
|
let cache_pool = state.cache_pool();
|
||||||
let token_key = state.env().token_key();
|
let token_key = state.config().secrets().token_key();
|
||||||
|
|
||||||
let trusted_token_str = auth_token::extract_token_string_from_http_headers(&parts.headers)?;
|
let trusted_token_str = auth_token::extract_token_string_from_http_headers(&parts.headers)?;
|
||||||
let trusted_token = verify_token(token_key, trusted_token_str, None)?;
|
let trusted_token = verify_token(token_key, trusted_token_str, None)?;
|
||||||
|
|
|
@ -32,7 +32,7 @@ pub async fn auth_login_post_handler(
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let db_pool = state.db_pool();
|
let db_pool = state.db_pool();
|
||||||
let cache_pool = state.cache_pool();
|
let cache_pool = state.cache_pool();
|
||||||
let token_key = state.env().token_key();
|
let token_key = state.config().secrets().token_key();
|
||||||
auth_login_request(db_pool, cache_pool, token_key, body).await
|
auth_login_request(db_pool, cache_pool, token_key, body).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub async fn auth_session_get_handler(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let cache_pool = state.cache_pool();
|
let cache_pool = state.cache_pool();
|
||||||
let token_key = state.env().token_key();
|
let token_key = state.config().secrets().token_key();
|
||||||
|
|
||||||
let auth_token_str = auth_token::extract_token_string_from_http_headers(&headers)?;
|
let auth_token_str = auth_token::extract_token_string_from_http_headers(&headers)?;
|
||||||
let auth_token = verify_token(token_key, auth_token_str, None)?;
|
let auth_token = verify_token(token_key, auth_token_str, None)?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
mod auth;
|
mod auth;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::{sync::mpsc::Sender, time::Duration};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{MatchedPath, Request},
|
extract::{MatchedPath, Request},
|
||||||
|
@ -16,23 +16,30 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::DbPool,
|
db::DbPool,
|
||||||
models::{AppError, Environment},
|
models::AppError,
|
||||||
services::CachePool,
|
services::{config::Config, CachePool, UserConfirmationMessage},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
db_pool: DbPool,
|
db_pool: DbPool,
|
||||||
cache_pool: CachePool,
|
cache_pool: CachePool,
|
||||||
env: Environment,
|
config: Config,
|
||||||
|
mail_sender: Sender<UserConfirmationMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(db_pool: DbPool, cache_pool: CachePool, env: Environment) -> Self {
|
pub fn new(
|
||||||
|
db_pool: DbPool,
|
||||||
|
cache_pool: CachePool,
|
||||||
|
config: Config,
|
||||||
|
mail_sender: Sender<UserConfirmationMessage>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db_pool,
|
db_pool,
|
||||||
cache_pool,
|
cache_pool,
|
||||||
env,
|
config,
|
||||||
|
mail_sender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,24 +51,25 @@ impl AppState {
|
||||||
&self.cache_pool
|
&self.cache_pool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn env(&self) -> &Environment {
|
pub fn config(&self) -> &Config {
|
||||||
&self.env
|
&self.config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mail_sender(&self) -> &Sender<UserConfirmationMessage> {
|
||||||
|
&self.mail_sender
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_app(
|
pub async fn start_app(
|
||||||
db_pool: DbPool,
|
db_pool: DbPool,
|
||||||
cache_pool: CachePool,
|
cache_pool: CachePool,
|
||||||
env: Environment,
|
config: Config,
|
||||||
|
mail_sender: Sender<UserConfirmationMessage>,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let address = env.hostname();
|
let connection_uri = config.app_connection_uri();
|
||||||
let port = env.port();
|
info!("Listening on {connection_uri}...");
|
||||||
|
|
||||||
info!("Listening on {address}:{port}...");
|
|
||||||
let listener = TcpListener::bind(format!("{address}:{port}"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(connection_uri).await.unwrap();
|
||||||
let logging_layer = TraceLayer::new_for_http()
|
let logging_layer = TraceLayer::new_for_http()
|
||||||
.make_span_with(|request: &Request| {
|
.make_span_with(|request: &Request| {
|
||||||
let path = request
|
let path = request
|
||||||
|
@ -82,7 +90,7 @@ pub async fn start_app(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = AppState::new(db_pool, cache_pool, env);
|
let state = AppState::new(db_pool, cache_pool, config, mail_sender);
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.merge(user::requests(state.clone()))
|
.merge(user::requests(state.clone()))
|
||||||
.merge(auth::requests(state.clone()))
|
.merge(auth::requests(state.clone()))
|
||||||
|
|
|
@ -26,15 +26,16 @@ pub async fn user_registration_post_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(request): Json<UserRegistrationRequest>,
|
Json(request): Json<UserRegistrationRequest>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let env = state.env();
|
let config = state.config();
|
||||||
|
let mail_sender = state.mail_sender();
|
||||||
|
|
||||||
register_new_user_request(
|
register_new_user_request(
|
||||||
request,
|
request,
|
||||||
state.db_pool(),
|
state.db_pool(),
|
||||||
state.cache_pool(),
|
state.cache_pool(),
|
||||||
env.token_key(),
|
config.secrets().token_key(),
|
||||||
env.send_verification_email(),
|
config.mail().send_verification_email,
|
||||||
env.email_sender(),
|
mail_sender,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,9 @@ pub async fn user_verification_get_handler(
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let db_pool = state.db_pool();
|
let db_pool = state.db_pool();
|
||||||
let cache_pool = state.cache_pool();
|
let cache_pool = state.cache_pool();
|
||||||
let env = state.env();
|
let token_key = state.config().secrets().token_key();
|
||||||
|
|
||||||
let UserVerifyGetParams { verification_token } = query;
|
let UserVerifyGetParams { verification_token } = query;
|
||||||
let token_key = env.token_key();
|
|
||||||
verify_new_user_request(db_pool, cache_pool, user_id, verification_token, token_key).await
|
verify_new_user_request(db_pool, cache_pool, user_id, verification_token, token_key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
185
api/src/services/config.rs
Normal file
185
api/src/services/config.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fmt::Display,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use http::Uri;
|
||||||
|
use pasetors::{keys::SymmetricKey, version4::V4};
|
||||||
|
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
|
||||||
|
|
||||||
|
use crate::models::AppError;
|
||||||
|
|
||||||
|
pub fn read_config() -> Result<Config, AppError> {
|
||||||
|
let cwd_config_file = env::current_dir()
|
||||||
|
.map(|cwd| cwd.join("debt-pirate.config.toml"))
|
||||||
|
.unwrap();
|
||||||
|
let base_override_config_file = PathBuf::from("/etc/debt-pirate/local.toml");
|
||||||
|
let base_config_file = PathBuf::from("/etc/debt-pirate/config.toml");
|
||||||
|
|
||||||
|
let config_file_path = match (
|
||||||
|
cwd_config_file.try_exists().unwrap_or_default(),
|
||||||
|
base_override_config_file.try_exists().unwrap_or_default(),
|
||||||
|
base_config_file.try_exists().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
(true, _, _) => cwd_config_file.as_path(),
|
||||||
|
(false, true, _) => base_override_config_file.as_path(),
|
||||||
|
(false, false, true) => base_config_file.as_path(),
|
||||||
|
(false, false, false) => return Err(AppError::missing_config_file()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Config::try_initialize_from_file(config_file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
hostname: String,
|
||||||
|
port: String,
|
||||||
|
assets_dir: PathBuf,
|
||||||
|
log: ConfigLogLevel,
|
||||||
|
database: ConfigDatabase,
|
||||||
|
cache: ConfigCache,
|
||||||
|
secrets: ConfigSecrets,
|
||||||
|
mail: ConfigMail,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn try_initialize_from_file(file: &Path) -> Result<Self, AppError> {
|
||||||
|
let buffer = fs::read_to_string(file)?;
|
||||||
|
toml::from_str(buffer.as_str()).map_err(AppError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_connection_uri(&self) -> String {
|
||||||
|
format!("{}:{}", self.hostname.as_str(), self.port.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db_connection_uri(&self) -> Uri {
|
||||||
|
self.database.connection_uri()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_connection_uri(&self) -> Uri {
|
||||||
|
self.cache.connection_uri()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logging_directive(&self) -> String {
|
||||||
|
format!("{}={}", env!("CARGO_PKG_NAME"), self.log)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assets_dir(&self) -> &Path {
|
||||||
|
self.assets_dir.as_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn secrets(&self) -> &ConfigSecrets {
|
||||||
|
&self.secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mail(&self) -> &ConfigMail {
|
||||||
|
&self.mail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ConfigLogLevel {
|
||||||
|
#[serde(rename = "trace")]
|
||||||
|
Trace,
|
||||||
|
|
||||||
|
#[serde(rename = "debug")]
|
||||||
|
Debug,
|
||||||
|
|
||||||
|
#[serde(rename = "info")]
|
||||||
|
Info,
|
||||||
|
|
||||||
|
#[serde(rename = "warn")]
|
||||||
|
Warn,
|
||||||
|
|
||||||
|
#[serde(rename = "error")]
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ConfigLogLevel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
Self::Trace => "trace",
|
||||||
|
Self::Debug => "debug",
|
||||||
|
Self::Info => "info",
|
||||||
|
Self::Warn => "warn",
|
||||||
|
Self::Error => "error",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ConfigDatabase {
|
||||||
|
host: String,
|
||||||
|
port: String,
|
||||||
|
user: String,
|
||||||
|
password: String,
|
||||||
|
schema: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigDatabase {
|
||||||
|
pub fn connection_uri(&self) -> Uri {
|
||||||
|
Uri::builder()
|
||||||
|
.scheme("postgres")
|
||||||
|
.authority(format!(
|
||||||
|
"{}:{}@{}:{}",
|
||||||
|
self.user, self.password, self.host, self.port
|
||||||
|
))
|
||||||
|
.path_and_query(format!("/{}", self.schema))
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ConfigCache {
|
||||||
|
host: String,
|
||||||
|
port: String,
|
||||||
|
user: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigCache {
|
||||||
|
pub fn connection_uri(&self) -> Uri {
|
||||||
|
Uri::builder()
|
||||||
|
.scheme("redis")
|
||||||
|
.authority(format!(
|
||||||
|
"{}:{}@{}:{}",
|
||||||
|
self.user, self.password, self.host, self.port
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ConfigSecrets {
|
||||||
|
#[serde(deserialize_with = "deserialize_symmetric_key")]
|
||||||
|
token_key: SymmetricKey<V4>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigSecrets {
|
||||||
|
pub fn token_key(&self) -> &SymmetricKey<V4> {
|
||||||
|
&self.token_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_symmetric_key<'de, D>(deserializer: D) -> Result<SymmetricKey<V4>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let buffer = String::deserialize(deserializer)?;
|
||||||
|
SymmetricKey::<V4>::try_from(buffer.as_str())
|
||||||
|
.map_err(|_| DeserializerError::custom("Invalid PASETO v4 local key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ConfigMail {
|
||||||
|
pub send_verification_email: bool,
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ use std::time::SystemTime;
|
||||||
|
|
||||||
use humantime::format_rfc3339_millis;
|
use humantime::format_rfc3339_millis;
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
use tracing::{level_filters::LevelFilter, Event, Subscriber};
|
use tracing::{level_filters::LevelFilter, Event, Level, Subscriber};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
fmt::{
|
fmt::{
|
||||||
format::{DefaultFields, Writer},
|
format::{DefaultFields, Writer},
|
||||||
|
@ -14,13 +14,12 @@ use tracing_subscriber::{
|
||||||
EnvFilter,
|
EnvFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::models::Environment;
|
use super::config::ConfigLogLevel;
|
||||||
|
|
||||||
pub fn initialize_logger(env: &Environment) {
|
pub fn initialize_logger(log_directive: String) {
|
||||||
let log_level = env.rust_log();
|
|
||||||
let env_filter = EnvFilter::builder()
|
let env_filter = EnvFilter::builder()
|
||||||
.with_default_directive(LevelFilter::INFO.into())
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
.parse_lossy(log_level);
|
.parse_lossy(log_directive);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod auth_token;
|
pub mod auth_token;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
pub mod config;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod mailer;
|
mod mailer;
|
||||||
mod password_hasher;
|
mod password_hasher;
|
||||||
|
|
Loading…
Add table
Reference in a new issue