Migrate away from .env, use a standard config file instead

This commit is contained in:
Z. Charles Dziura 2025-02-12 11:28:23 -05:00
parent cac66699d1
commit 1f3ce078e1
18 changed files with 308 additions and 330 deletions

2
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -1,3 +0,0 @@
TOKEN_KEY=
DB_CONNECTION_URI=
ASSETS_DIR=

View file

@ -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"

View 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

View file

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

View file

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

View file

@ -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,

View file

@ -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::*;

View file

@ -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)?;

View file

@ -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
}

View file

@ -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)?;

View file

@ -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()))

View file

@ -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
}

View file

@ -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
View 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,
}

View file

@ -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()

View file

@ -1,5 +1,6 @@
pub mod auth_token;
mod cache;
pub mod config;
mod logger;
mod mailer;
mod password_hasher;