Skip to content
Clone

feat(lib/config): add web, auth, and pow config modules

ZhenYi committed
parent 13582e8 commit 9af5bb3 Browse files

5 files changed

+260 −1

File tree 5
Added lib/config/src/auth.rs View lib/config/src/auth.rs
@@ -0,0 +1,166 @@
1use base64::Engine;
2use serde::Deserialize;
3
4#[derive(Debug, Clone, Deserialize)]
5#[serde(default)]
6pub struct PasswordPolicy {
7 pub min_length: usize,
8 pub require_upper: bool,
9 pub require_lower: bool,
10 pub require_digit: bool,
11 pub require_symbol: bool,
12}
13
14impl Default for PasswordPolicy {
15 fn default() -> Self {
16 Self {
17 min_length: 8,
18 require_upper: true,
19 require_lower: true,
20 require_digit: true,
21 require_symbol: false,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Default, Deserialize)]
27#[serde(default)]
28pub struct AgreementDocConfig {
29 pub key: String,
30 pub version: String,
31 pub title: String,
32 pub content_md: String,
33}
34
35#[derive(Debug, Clone, Deserialize)]
36#[serde(default)]
37pub struct AuthConfig {
38 pub password_policy: PasswordPolicy,
39 pub verify_code_ttl_secs: u64,
40 pub verify_code_len: usize,
41 pub reset_token_ttl_secs: u64,
42 pub reset_token_bytes: usize,
43 pub email_code_rate_limit_secs: u64,
44 pub max_verify_attempts: u64,
45 pub min_age: u8,
46 pub totp_encryption_key: Option<String>,
47 pub agreements: Vec<AgreementDocConfig>,
48}
49
50impl Default for AuthConfig {
51 fn default() -> Self {
52 Self {
53 password_policy: PasswordPolicy::default(),
54 verify_code_ttl_secs: 600,
55 verify_code_len: 6,
56 reset_token_ttl_secs: 1800,
57 reset_token_bytes: 32,
58 email_code_rate_limit_secs: 60,
59 max_verify_attempts: 5,
60 min_age: 13,
61 totp_encryption_key: None,
62 agreements: vec![
63 AgreementDocConfig {
64 key: "terms".to_string(),
65 version: "0.1.0".to_string(),
66 title: "Terms of Service".to_string(),
67 content_md: "Placeholder terms of service.".to_string(),
68 },
69 AgreementDocConfig {
70 key: "privacy".to_string(),
71 version: "0.1.0".to_string(),
72 title: "Privacy Policy".to_string(),
73 content_md: "Placeholder privacy policy.".to_string(),
74 },
75 ],
76 }
77 }
78}
79
80impl AuthConfig {
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn with_password_policy(mut self, policy: PasswordPolicy) -> Self {
86 self.password_policy = policy;
87 self
88 }
89
90 pub fn with_verify_code_ttl_secs(mut self, secs: u64) -> Self {
91 self.verify_code_ttl_secs = secs;
92 self
93 }
94
95 pub fn with_verify_code_len(mut self, len: usize) -> Self {
96 self.verify_code_len = len;
97 self
98 }
99
100 pub fn with_reset_token_ttl_secs(mut self, secs: u64) -> Self {
101 self.reset_token_ttl_secs = secs;
102 self
103 }
104
105 pub fn with_reset_token_bytes(mut self, bytes: usize) -> Self {
106 self.reset_token_bytes = bytes;
107 self
108 }
109
110 pub fn with_email_code_rate_limit_secs(mut self, secs: u64) -> Self {
111 self.email_code_rate_limit_secs = secs;
112 self
113 }
114
115 pub fn with_max_verify_attempts(mut self, attempts: u64) -> Self {
116 self.max_verify_attempts = attempts;
117 self
118 }
119
120 pub fn with_min_age(mut self, min_age: u8) -> Self {
121 self.min_age = min_age;
122 self
123 }
124
125 pub fn with_totp_encryption_key(mut self, key: impl Into<String>) -> Self {
126 self.totp_encryption_key = Some(key.into());
127 self
128 }
129
130 pub fn with_agreements(mut self, agreements: Vec<AgreementDocConfig>) -> Self {
131 self.agreements = agreements;
132 self
133 }
134
135 pub fn validate(&self) -> Result<(), String> {
136 if !(4..=10).contains(&self.verify_code_len) {
137 return Err("auth.verify_code_len must be between 4 and 10".to_string());
138 }
139 if self.verify_code_ttl_secs == 0 {
140 return Err("auth.verify_code_ttl_secs must be greater than 0".to_string());
141 }
142 if self.reset_token_ttl_secs == 0 {
143 return Err("auth.reset_token_ttl_secs must be greater than 0".to_string());
144 }
145 if self.reset_token_bytes < 16 {
146 return Err("auth.reset_token_bytes must be at least 16".to_string());
147 }
148 if self.email_code_rate_limit_secs == 0 {
149 return Err("auth.email_code_rate_limit_secs must be greater than 0".to_string());
150 }
151 if self.max_verify_attempts == 0 {
152 return Err("auth.max_verify_attempts must be greater than 0".to_string());
153 }
154 if let Some(key) = &self.totp_encryption_key {
155 let length = base64::engine::general_purpose::STANDARD
156 .decode(key)
157 .map(|bytes| bytes.len())
158 .unwrap_or(0);
159 if length < 32 {
160 return Err("auth.totp_encryption_key must be base64 of at least 32 bytes"
161 .to_string());
162 }
163 }
164 Ok(())
165 }
166}
No newline at end of file
Modified lib/config/src/lib.rs View lib/config/src/lib.rs
@@ -1,21 +1,33 @@
1use crate::auth::AuthConfig;
12use crate::cache::CacheConfig;
23use crate::database::DatabaseConfig;
4use crate::pow::PowConfig;
35use crate::redis::RedisConfig;
46use crate::s3::S3Config;
57use crate::session::SessionConfig;
68
9use serde::Deserialize;
10use crate::web::WebConfig;
11
12pub mod auth;
713pub mod s3;
814pub mod redis;
915pub mod cache;
1016pub mod database;
1117pub mod session;
18pub mod pow;
1219pub mod mailbaby;
20pub mod web;
1321
14#[derive(Clone,Debug)]
22#[derive(Clone, Debug, Default, Deserialize)]
23#[serde(default)]
1524pub struct AppConfig {
1625 pub s3: S3Config,
1726 pub redis: RedisConfig,
1827 pub cache: CacheConfig,
1928 pub database: DatabaseConfig,
2029 pub session: SessionConfig,
30 pub pow: PowConfig,
31 pub auth: AuthConfig,
32 pub web: WebConfig,
2133}
No newline at end of file
Added lib/config/src/pow.rs View lib/config/src/pow.rs
@@ -0,0 +1,73 @@
1use base64::Engine;
2use serde::Deserialize;
3
4#[derive(Debug, Clone, Deserialize)]
5#[serde(default)]
6pub struct PowConfig {
7 pub difficulty: u32,
8 pub max_difficulty: u32,
9 pub expires_secs: u64,
10 pub mac_key: Option<String>,
11}
12
13impl Default for PowConfig {
14 fn default() -> Self {
15 Self {
16 difficulty: 16,
17 max_difficulty: 24,
18 expires_secs: 60,
19 mac_key: None,
20 }
21 }
22}
23
24impl PowConfig {
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn with_difficulty(mut self, difficulty: u32) -> Self {
30 self.difficulty = difficulty;
31 self
32 }
33
34 pub fn with_max_difficulty(mut self, max_difficulty: u32) -> Self {
35 self.max_difficulty = max_difficulty;
36 self
37 }
38
39 pub fn with_expires_secs(mut self, expires_secs: u64) -> Self {
40 self.expires_secs = expires_secs;
41 self
42 }
43
44 pub fn with_mac_key(mut self, mac_key: impl Into<String>) -> Self {
45 self.mac_key = Some(mac_key.into());
46 self
47 }
48
49 pub fn validate(&self) -> Result<(), String> {
50 if self.max_difficulty > 256 {
51 return Err("pow.max_difficulty must be <= 256".to_string());
52 }
53 if self.difficulty > self.max_difficulty {
54 return Err("pow.difficulty must be <= pow.max_difficulty".to_string());
55 }
56 if self.expires_secs == 0 {
57 return Err("pow.expires_secs must be greater than 0".to_string());
58 }
59 match &self.mac_key {
60 Some(key) => {
61 let length = base64::engine::general_purpose::STANDARD
62 .decode(key)
63 .map(|bytes| bytes.len())
64 .unwrap_or(0);
65 if length < 32 {
66 return Err("pow.mac_key must be base64 of at least 32 bytes".to_string());
67 }
68 }
69 None => return Err("pow.mac_key is required".to_string()),
70 }
71 Ok(())
72 }
73}
No newline at end of file
Added lib/config/src/web.rs View lib/config/src/web.rs
@@ -0,0 +1,7 @@
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4#[serde(default)]
5pub struct WebConfig {
6 pub web_base_url: String,
7}
No newline at end of file
Modified lib/config/Cargo.toml View lib/config/Cargo.toml
@@ -6,3 +6,4 @@
66[dependencies]
77serde = { workspace = true, features = ["derive"] }
88sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
9base64 = { workspace = true }