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
|
||||
.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]
|
||||
argon2 = "0.5"
|
||||
axum = { version = "0.7", features = [
|
||||
axum = { version = "0.8", features = [
|
||||
"macros",
|
||||
"multipart",
|
||||
"ws",
|
||||
] }
|
||||
base64 = "0.22"
|
||||
bb8-redis = "0.17"
|
||||
bb8-redis = "0.20"
|
||||
blake3 = { version = "1.5", features = ["serde"] }
|
||||
dotenvy = "0.15"
|
||||
futures = "0.3"
|
||||
http = "1.0"
|
||||
humantime = "2.1"
|
||||
|
@ -29,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [
|
|||
] }
|
||||
num_cpus = "1.16"
|
||||
pasetors = "0.7"
|
||||
redis = { version = "0.27", features = ["aio"] }
|
||||
redis = { version = "0.28", features = ["aio"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
||||
serde_json = "1.0"
|
||||
serde_with = "3.9"
|
||||
|
@ -42,6 +41,7 @@ sqlx = { version = "0.8", features = [
|
|||
syslog-tracing = "0.3.1"
|
||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||
tokio = { version = "1.35", features = ["full"] }
|
||||
toml = "0.8"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["full"] }
|
||||
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 models::Environment;
|
||||
|
||||
mod db;
|
||||
mod models;
|
||||
mod requests;
|
||||
|
@ -17,29 +15,20 @@ use tracing::{error, info};
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let (tx, rx) = channel::<UserConfirmationMessage>();
|
||||
|
||||
let env = match Environment::init(tx) {
|
||||
Ok(env) => env,
|
||||
let config = match services::config::read_config() {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
initialize_logger(&env);
|
||||
initialize_logger(config.logging_directive());
|
||||
|
||||
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!("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...");
|
||||
if let Err(err) = run_migrations(&db_pool).await {
|
||||
eprintln!("{err:?}");
|
||||
|
@ -47,11 +36,19 @@ async fn main() {
|
|||
}
|
||||
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...");
|
||||
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.");
|
||||
|
||||
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:?}");
|
||||
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 bb8_redis::bb8::RunError;
|
||||
use http::StatusCode;
|
||||
use redis::RedisError;
|
||||
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
|
||||
use toml::{self, de::Error as TomlError};
|
||||
use tracing::trace;
|
||||
|
||||
use super::ApiResponse;
|
||||
|
@ -49,8 +55,8 @@ impl AppError {
|
|||
Self::new(ErrorKind::MissingAuthorizationToken)
|
||||
}
|
||||
|
||||
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
||||
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
||||
pub fn missing_config_file() -> Self {
|
||||
Self::new(ErrorKind::MissingConfigFile)
|
||||
}
|
||||
|
||||
pub fn missing_session_field(field: &'static str) -> Self {
|
||||
|
@ -65,10 +71,6 @@ impl AppError {
|
|||
Self::new(ErrorKind::NoUserFound)
|
||||
}
|
||||
|
||||
pub fn token_key() -> Self {
|
||||
Self::new(ErrorKind::TokenKey)
|
||||
}
|
||||
|
||||
pub fn is_duplicate_record(&self) -> bool {
|
||||
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 {
|
||||
fn from(other: MigrateError) -> Self {
|
||||
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 {
|
||||
fn from(other: SqlxError) -> Self {
|
||||
match &other {
|
||||
|
@ -107,17 +129,9 @@ impl From<SqlxError> for AppError {
|
|||
}
|
||||
}
|
||||
|
||||
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<TomlError> for AppError {
|
||||
fn from(other: TomlError) -> Self {
|
||||
Self::new(ErrorKind::ConfigParse(other))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,6 +140,10 @@ impl Display for AppError {
|
|||
match &self.kind {
|
||||
ErrorKind::AppStartupError(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!(
|
||||
f,
|
||||
"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::InvalidCredentials => write!(f, "Invalid email address or password"),
|
||||
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::MissingEnvironmentVariables(missing_vars) => write!(
|
||||
f,
|
||||
"Missing required environment variables: {}",
|
||||
missing_vars.join(", ")
|
||||
),
|
||||
ErrorKind::MissingSessionField(field) => write!(
|
||||
f,
|
||||
"Cannot retrieve session: missing required field: {field}"
|
||||
|
@ -168,6 +182,8 @@ impl Display for AppError {
|
|||
enum ErrorKind {
|
||||
AppStartupError(io::Error),
|
||||
Cache(String),
|
||||
ConfigFile(IoError),
|
||||
ConfigParse(TomlError),
|
||||
ConnectionInfo(&'static str),
|
||||
Database,
|
||||
DbMigration(MigrateError),
|
||||
|
@ -175,7 +191,7 @@ enum ErrorKind {
|
|||
ExpiredToken,
|
||||
InvalidCredentials,
|
||||
InvalidToken,
|
||||
MissingEnvironmentVariables(Vec<&'static str>),
|
||||
MissingConfigFile,
|
||||
MissingSessionField(&'static str),
|
||||
MissingAuthorizationToken,
|
||||
NoDbRecordFound,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
mod api_response;
|
||||
mod environment;
|
||||
mod error;
|
||||
mod session;
|
||||
|
||||
pub use api_response::*;
|
||||
pub use environment::*;
|
||||
pub use error::*;
|
||||
pub use session::*;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use axum::{async_trait, extract::FromRequestParts};
|
||||
use axum::extract::FromRequestParts;
|
||||
use http::request::Parts;
|
||||
use humantime::format_rfc3339;
|
||||
use redis::ToRedisArgs;
|
||||
|
@ -41,7 +41,6 @@ impl Session {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<AppState> for Session {
|
||||
type Rejection = AppError;
|
||||
|
||||
|
@ -50,7 +49,7 @@ impl FromRequestParts<AppState> for Session {
|
|||
state: &'b AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
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 = verify_token(token_key, trusted_token_str, None)?;
|
||||
|
|
|
@ -32,7 +32,7 @@ pub async fn auth_login_post_handler(
|
|||
) -> Result<Response, AppError> {
|
||||
let db_pool = state.db_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
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ pub async fn auth_session_get_handler(
|
|||
headers: HeaderMap,
|
||||
) -> Result<Response, AppError> {
|
||||
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 = verify_token(token_key, auth_token_str, None)?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
mod auth;
|
||||
mod user;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{sync::mpsc::Sender, time::Duration};
|
||||
|
||||
use axum::{
|
||||
extract::{MatchedPath, Request},
|
||||
|
@ -16,23 +16,30 @@ use uuid::Uuid;
|
|||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
models::{AppError, Environment},
|
||||
services::CachePool,
|
||||
models::AppError,
|
||||
services::{config::Config, CachePool, UserConfirmationMessage},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
db_pool: DbPool,
|
||||
cache_pool: CachePool,
|
||||
env: Environment,
|
||||
config: Config,
|
||||
mail_sender: Sender<UserConfirmationMessage>,
|
||||
}
|
||||
|
||||
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 {
|
||||
db_pool,
|
||||
cache_pool,
|
||||
env,
|
||||
config,
|
||||
mail_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,24 +51,25 @@ impl AppState {
|
|||
&self.cache_pool
|
||||
}
|
||||
|
||||
pub fn env(&self) -> &Environment {
|
||||
&self.env
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn mail_sender(&self) -> &Sender<UserConfirmationMessage> {
|
||||
&self.mail_sender
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_app(
|
||||
db_pool: DbPool,
|
||||
cache_pool: CachePool,
|
||||
env: Environment,
|
||||
config: Config,
|
||||
mail_sender: Sender<UserConfirmationMessage>,
|
||||
) -> Result<(), AppError> {
|
||||
let address = env.hostname();
|
||||
let port = env.port();
|
||||
|
||||
info!("Listening on {address}:{port}...");
|
||||
let listener = TcpListener::bind(format!("{address}:{port}"))
|
||||
.await
|
||||
.unwrap();
|
||||
let connection_uri = config.app_connection_uri();
|
||||
info!("Listening on {connection_uri}...");
|
||||
|
||||
let listener = TcpListener::bind(connection_uri).await.unwrap();
|
||||
let logging_layer = TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &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()
|
||||
.merge(user::requests(state.clone()))
|
||||
.merge(auth::requests(state.clone()))
|
||||
|
|
|
@ -26,15 +26,16 @@ pub async fn user_registration_post_handler(
|
|||
State(state): State<AppState>,
|
||||
Json(request): Json<UserRegistrationRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let env = state.env();
|
||||
let config = state.config();
|
||||
let mail_sender = state.mail_sender();
|
||||
|
||||
register_new_user_request(
|
||||
request,
|
||||
state.db_pool(),
|
||||
state.cache_pool(),
|
||||
env.token_key(),
|
||||
env.send_verification_email(),
|
||||
env.email_sender(),
|
||||
config.secrets().token_key(),
|
||||
config.mail().send_verification_email,
|
||||
mail_sender,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -31,10 +31,9 @@ pub async fn user_verification_get_handler(
|
|||
) -> Result<Response, AppError> {
|
||||
let db_pool = state.db_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 token_key = env.token_key();
|
||||
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 time::macros::format_description;
|
||||
use tracing::{level_filters::LevelFilter, Event, Subscriber};
|
||||
use tracing::{level_filters::LevelFilter, Event, Level, Subscriber};
|
||||
use tracing_subscriber::{
|
||||
fmt::{
|
||||
format::{DefaultFields, Writer},
|
||||
|
@ -14,13 +14,12 @@ use tracing_subscriber::{
|
|||
EnvFilter,
|
||||
};
|
||||
|
||||
use crate::models::Environment;
|
||||
use super::config::ConfigLogLevel;
|
||||
|
||||
pub fn initialize_logger(env: &Environment) {
|
||||
let log_level = env.rust_log();
|
||||
pub fn initialize_logger(log_directive: String) {
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.parse_lossy(log_level);
|
||||
.parse_lossy(log_directive);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
tracing_subscriber::fmt()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod auth_token;
|
||||
mod cache;
|
||||
pub mod config;
|
||||
mod logger;
|
||||
mod mailer;
|
||||
mod password_hasher;
|
||||
|
|
Loading…
Add table
Reference in a new issue