pub mod validate;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::error::{CoreError, InternalError};
use super::boot::AppConfig;
use super::boot::cache::CacheDriver;
use super::boot::database::DatabaseDriver;
use super::validate::Validate;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct AnzarConfiguration {
pub app: App, pub database: Database, #[serde(default)]
pub server: Server, #[serde(default)]
pub auth: Authentication, pub security: Security, }
impl AnzarConfiguration {
pub fn validate(&self) -> Result<(), CoreError> {
let mut errors = vec![];
if let Err(e) = self.auth.validate() {
errors.extend(e);
}
if let Err(e) = self.security.validate() {
errors.extend(e);
}
if errors.is_empty() {
Ok(())
} else {
Err(CoreError::Internal(InternalError::InvalidConfig(errors)))
}
}
}
impl AnzarConfiguration {
pub fn new(app_config: AppConfig) -> Self {
Self {
app: App {
environment: "dev".into(),
url: "localhost:3000".to_string(),
},
database: Database {
driver: app_config.database.driver,
connection_string: app_config.database.connection_string(),
cache: Cache {
driver: app_config.cache.driver,
url: app_config.cache.url,
},
},
server: Server::default(),
auth: Authentication {
strategy: app_config.auth,
..Default::default()
},
security: Security {
secret_key: String::default(),
rate_limit: RateLimit {
enabled: true,
ip: RateLimitConfig::ip(),
strict: RateLimitConfig {
duration_minutes: 60,
capacity: 7,
},
default: RateLimitConfig::defaults(),
},
headers: vec![],
auth: AuthSecurity {
max_failed_attempts: 5,
lockout_duration: 1800,
},
},
}
}
pub fn with_appurl(mut self, url: &str) -> Self {
self.app.url = url.to_string();
self
}
pub fn with_secret(mut self, key: &str) -> Self {
self.security.secret_key = key.to_string();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct App {
pub environment: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct Database {
pub driver: DatabaseDriver,
pub connection_string: String,
pub cache: Cache,
}
impl Database {
pub fn name(&self) -> Option<&str> {
self.connection_string
.rsplit('/')
.next()
.and_then(|s| s.split('?').next())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct Cache {
pub driver: CacheDriver,
pub url: String,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct Server {
pub https: HttpsConfig,
pub cors: CorsConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct HttpsConfig {
pub enabled: bool,
pub port: u16,
pub cert_path: Option<String>,
pub key_path: Option<String>,
}
impl Default for HttpsConfig {
fn default() -> Self {
Self {
enabled: false,
port: 3000,
cert_path: None,
key_path: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct CorsConfig {
pub enabled: bool,
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<String>,
pub allowed_headers: Vec<String>,
pub allow_credentials: bool,
pub max_age: u64,
}
impl Default for CorsConfig {
fn default() -> Self {
Self {
enabled: true,
allowed_origins: vec!["localhost:3000".into()],
allowed_methods: vec![
"GET".into(),
"POST".into(),
"PUT".into(),
"DELETE".into(),
"OPTIONS".into(),
],
allowed_headers: vec![
"authorization".into(),
"content-type".into(),
"accept".into(),
"accept-language".into(),
"Content-Language".into(),
],
allow_credentials: true,
max_age: 3600,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct Authentication {
pub strategy: AuthStrategy,
pub email: EmailConfig,
pub password: PasswordConfig,
pub rbac: RbacConfig,
}
impl Authentication {
pub fn jwt(&self) -> Result<&JwtConfig, CoreError> {
match &self.strategy {
AuthStrategy::Jwt(config) => Ok(config),
_ => Err(CoreError::Internal(InternalError::MissingConfiguration(
"JWT strategy is required, but auth.strategy was not configured correctly".into(),
))),
}
}
pub fn session(&self) -> Result<&SessionConfig, CoreError> {
match &self.strategy {
AuthStrategy::Session(config) => Ok(config),
_ => Err(CoreError::Internal(InternalError::MissingConfiguration(
"Session strategy is required, but auth.strategy was not configured correctly"
.into(),
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(tag = "type")]
pub enum AuthStrategy {
Session(SessionConfig),
Jwt(JwtConfig),
}
impl Default for AuthStrategy {
fn default() -> Self {
Self::Session(SessionConfig::default())
}
}
impl std::fmt::Display for AuthStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthStrategy::Session(_) => write!(f, "Session"),
AuthStrategy::Jwt(_) => write!(f, "Jwt"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct JwtConfig {
pub algorithm: AlgorithmConfig,
pub access_token_expires_in: i64,
pub refresh_token_expires_in: i64,
pub issuer: String,
pub audience: String,
}
impl Default for JwtConfig {
fn default() -> Self {
Self {
algorithm: AlgorithmConfig::default(),
access_token_expires_in: 900,
refresh_token_expires_in: 604800,
issuer: String::new(),
audience: String::new(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub enum AlgorithmConfig {
ES256,
ES384,
#[default]
RS256,
RS384,
RS512,
PS256,
PS384,
PS512,
EdDSA,
}
impl AlgorithmConfig {
pub fn as_str(&self) -> &'static str {
match self {
AlgorithmConfig::ES256 => "ES256",
AlgorithmConfig::ES384 => "ES384",
AlgorithmConfig::RS256 => "RS256",
AlgorithmConfig::RS384 => "RS384",
AlgorithmConfig::RS512 => "RS512",
AlgorithmConfig::PS256 => "PS256",
AlgorithmConfig::PS384 => "PS384",
AlgorithmConfig::PS512 => "PS512",
AlgorithmConfig::EdDSA => "EdDSA",
}
}
}
impl From<AlgorithmConfig> for jsonwebtoken::Algorithm {
fn from(value: AlgorithmConfig) -> Self {
match value {
AlgorithmConfig::ES256 => jsonwebtoken::Algorithm::ES256,
AlgorithmConfig::ES384 => jsonwebtoken::Algorithm::ES384,
AlgorithmConfig::RS256 => jsonwebtoken::Algorithm::RS256,
AlgorithmConfig::RS384 => jsonwebtoken::Algorithm::RS384,
AlgorithmConfig::PS256 => jsonwebtoken::Algorithm::PS256,
AlgorithmConfig::PS384 => jsonwebtoken::Algorithm::PS384,
AlgorithmConfig::PS512 => jsonwebtoken::Algorithm::PS512,
AlgorithmConfig::RS512 => jsonwebtoken::Algorithm::RS512,
AlgorithmConfig::EdDSA => jsonwebtoken::Algorithm::EdDSA,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct SessionConfig {
pub name: String,
pub max_age: u64,
pub secure: bool,
pub http_only: bool,
pub same_site: SameSiteConfig,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub enum SameSiteConfig {
#[default]
Strict,
Lax,
None,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
name: "id".into(),
max_age: 3600,
secure: true,
http_only: true,
same_site: SameSiteConfig::default(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct EmailConfig {
pub verification: EmailVerification,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct EmailVerification {
pub required: bool,
pub token_expires_in: i64, pub success_redirect: Option<String>,
pub error_redirect: Option<String>,
}
impl Default for EmailVerification {
fn default() -> Self {
Self {
required: false,
token_expires_in: 1800,
success_redirect: None,
error_redirect: None,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct PasswordConfig {
pub algorithm: HashingAlgorithm,
pub requirements: PasswordRequirements,
pub reset: PasswordReset,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(tag = "type")]
pub enum HashingAlgorithm {
Argon2 {
memory_kib: u32,
iterations: u32,
parallelism: u32,
},
Bcrypt {
cost: u32,
},
}
impl Default for HashingAlgorithm {
fn default() -> Self {
pub const DEFAULT_M_COST: u32 = 19 * 1024; pub const DEFAULT_T_COST: u32 = 2;
pub const DEFAULT_P_COST: u32 = 1;
Self::Argon2 {
memory_kib: DEFAULT_M_COST,
iterations: DEFAULT_T_COST,
parallelism: DEFAULT_P_COST,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct PasswordRequirements {
pub min_length: u16,
pub max_length: u16,
pub require_uppercase: bool,
pub require_number: bool,
pub require_special_char: bool,
}
impl Default for PasswordRequirements {
fn default() -> Self {
Self {
min_length: 8,
max_length: 128,
require_uppercase: false,
require_number: false,
require_special_char: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct PasswordReset {
pub token_expires_in: i64, pub success_redirect: Option<String>,
pub error_redirect: Option<String>,
}
impl Default for PasswordReset {
fn default() -> Self {
Self {
token_expires_in: 1800,
success_redirect: None,
error_redirect: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct RbacConfig {
pub enabled: bool,
pub default_role: String,
pub roles: Vec<RoleConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct RoleConfig {
pub name: String,
#[serde(default)]
pub inherits: Vec<String>,
pub permissions: Vec<String>,
}
impl Default for RbacConfig {
fn default() -> Self {
Self {
enabled: false,
default_role: "user".into(),
roles: vec![RoleConfig {
name: "user".into(),
inherits: vec![],
permissions: vec!["*:read".into()],
}],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct Security {
#[serde(skip_serializing)]
pub secret_key: String,
#[serde(default)]
pub auth: AuthSecurity,
#[serde(default)]
pub rate_limit: RateLimit,
#[serde(default = "default_headers")]
pub headers: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(default)]
pub struct AuthSecurity {
pub max_failed_attempts: u8,
pub lockout_duration: i64,
}
impl Default for AuthSecurity {
fn default() -> Self {
Self {
max_failed_attempts: 5,
lockout_duration: 1800,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct RateLimit {
#[serde(default)]
pub enabled: bool,
#[serde(default = "RateLimitConfig::ip")]
pub ip: RateLimitConfig,
#[serde(default = "RateLimitConfig::strict")]
pub strict: RateLimitConfig,
#[serde(default = "RateLimitConfig::defaults")]
pub default: RateLimitConfig,
}
impl Default for RateLimit {
fn default() -> Self {
Self {
enabled: false,
ip: RateLimitConfig::ip(),
strict: RateLimitConfig::strict(),
default: RateLimitConfig::defaults(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
pub struct RateLimitConfig {
pub capacity: u32,
pub duration_minutes: u32,
}
impl RateLimitConfig {
fn ip() -> RateLimitConfig {
RateLimitConfig {
duration_minutes: 1,
capacity: 100,
}
}
fn strict() -> RateLimitConfig {
RateLimitConfig {
duration_minutes: 60,
capacity: 5,
}
}
fn defaults() -> RateLimitConfig {
RateLimitConfig {
duration_minutes: 15,
capacity: 20,
}
}
}
fn default_headers() -> Vec<(String, String)> {
vec![
("X-Content-Type-Options".into(), "nosniff".into()),
("X-Frame-Options".into(), "DENY".into()),
("X-XSS-Protection".into(), "0".into()),
("Cache-Control".into(), "no-store".into()),
("Pragma".into(), "no-cache".into()),
(
"Content-Security-Policy".into(),
"default-src 'self'".into(),
),
("Content-Type".into(), "application/json".into()),
(
"Strict-Transport-Security".into(),
"max-age=31536000".into(),
),
]
}