feat(config): add central lib/config crate consolidating per-crate config modules
Introduces a single `lib/config` crate that replaces the scattered `config.rs` in every workspace member. Each domain (auth, cache, chat, db, email, git, issues, jwt, payment, pow, project, queue, rate_limit, repo, search, space, storage, user) gets its own typed config section, loaded from a single source via `loader.rs`. 24 files: Cargo.toml + 23 typed config sections + loader.
zhenyi committed
Added lib/config/src/app.rs lib/config/src/app.rs +70 −0 View lib/config/src/app.rs
@@ -0,0 +1,70 @@
1use serde::{Deserialize, Serialize};
2
3use crate::auth;
4use crate::cache;
5use crate::chat;
6use crate::db;
7use crate::email;
8use crate::git;
9use crate::issues;
10use crate::jwt;
11use crate::payment;
12use crate::pow;
13use crate::project;
14use crate::queue;
15use crate::rate_limit;
16use crate::repo;
17use crate::search;
18use crate::space;
19use crate::storage;
20use crate::user;
21
22#[derive(Clone, Debug, Default, Deserialize, Serialize)]
23#[serde(default)]
24pub struct AppConfig {
25 pub db: db::AppDatabaseSettings,
26 pub cache: cache::AppRedisSettings,
27 pub app_cache: cache::AppCacheSettings,
28 pub queue: queue::AppQueueSettings,
29 pub email: email::AppEmailSettings,
30 pub storage: storage::StorageSettings,
31 pub jwt: jwt::JwtSettings,
32 pub auth: auth::AuthConfig,
33 pub pow: pow::PowSettings,
34 pub issues: issues::IssueConfig,
35 pub project: project::ProjectConfig,
36 pub repo: repo::RepoConfig,
37 pub space: space::SpaceConfig,
38 pub user: user::UserConfig,
39 pub payment: payment::PaymentConfig,
40 pub rate_limit: rate_limit::RateLimitConfig,
41 pub search: search::AppSearchSettings,
42 pub chat: chat::AppChatSettings,
43 pub gateway: chat::GatewaySettings,
44 pub git_smart: git::SmartConfig,
45 pub git_ssh: git::GitSshConfig,
46 pub git_auth: git::GitAuthConfig,
47 pub git_sync: git::SyncSettings,
48}
49
50impl AppConfig {
51 pub fn validate(&self) -> Result<(), String> {
52 self.email.is_valid()?;
53 self.queue.is_valid()?;
54 self.search.is_valid()?;
55 if self.chat.topic_prefix != self.gateway.topic_prefix {
56 return Err(format!(
57 "chat.topic_prefix ({:?}) must equal gateway.topic_prefix ({:?})",
58 self.chat.topic_prefix, self.gateway.topic_prefix
59 ));
60 }
61 if self.repo.git_storage_root.is_empty()
62 || self.repo.lfs_storage_root.is_empty()
63 || self.git_smart.git_storage_root.is_empty()
64 || self.git_ssh.git_storage_root.is_empty()
65 {
66 return Err("git_storage_root / lfs_storage_root must not be empty".into());
67 }
68 Ok(())
69 }
70}
No newline at end of file
Added lib/config/src/auth.rs lib/config/src/auth.rs +203 −0 View lib/config/src/auth.rs
@@ -0,0 +1,203 @@
1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PasswordPolicy {
9 pub min_length: usize,
10 #[serde(default = "default_true")]
11 pub require_upper: bool,
12 #[serde(default = "default_true")]
13 pub require_lower: bool,
14 #[serde(default = "default_true")]
15 pub require_digit: bool,
16 #[serde(default)]
17 pub require_symbol: bool,
18}
19
20fn default_true() -> bool {
21 true
22}
23
24impl Default for PasswordPolicy {
25 fn default() -> Self {
26 Self {
27 min_length: 8,
28 require_upper: true,
29 require_lower: true,
30 require_digit: true,
31 require_symbol: false,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct OAuthProviderConfig {
38 #[serde(default)]
39 pub enabled: bool,
40 #[serde(default)]
41 pub client_id: String,
42 #[serde(default)]
43 pub client_secret: String,
44 #[serde(default)]
45 pub redirect_uri: String,
46 #[serde(default)]
47 pub scopes: Vec<String>,
48 #[serde(default)]
49 pub authorize_url: String,
50 #[serde(default)]
51 pub token_url: String,
52 #[serde(default)]
53 pub userinfo_url: String,
54 #[serde(default)]
55 pub emails_url: Option<String>,
56 #[serde(default)]
57 pub extra: BTreeMap<String, String>,
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct OAuthConfig {
62 #[serde(default)]
63 pub google: Option<OAuthProviderConfig>,
64 #[serde(default)]
65 pub github: Option<OAuthProviderConfig>,
66 #[serde(default)]
67 pub apple: Option<OAuthProviderConfig>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
71pub struct AgreementDoc {
72 pub key: String,
73 pub version: String,
74 pub updated_at: String,
75 pub title: String,
76 pub content_md: String,
77}
78
79impl AgreementDoc {
80 pub fn placeholder(key: &str) -> Self {
81 Self {
82 key: key.to_string(),
83 version: "0.1.0".to_string(),
84 updated_at: "1970-01-01T00:00:00Z".to_string(),
85 title: format!("{} (placeholder)", key),
86 content_md: format!(
87 "# {}\n\nThis document is a placeholder. Please replace before production.",
88 key
89 ),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct AgreementsConfig {
96 pub terms: AgreementDoc,
97 pub privacy: AgreementDoc,
98}
99
100impl AgreementsConfig {
101 pub fn iter(&self) -> impl Iterator<Item = &AgreementDoc> {
102 vec![&self.terms, &self.privacy].into_iter()
103 }
104}
105
106impl Default for AgreementsConfig {
107 fn default() -> Self {
108 Self {
109 terms: AgreementDoc::placeholder("terms"),
110 privacy: AgreementDoc::placeholder("privacy"),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AuthConfig {
117 #[serde(default)]
118 pub password_policy: PasswordPolicy,
119 #[serde(default = "default_verify_code_ttl")]
120 pub verify_code_ttl_secs: u64,
121 #[serde(default = "default_verify_code_len")]
122 pub verify_code_len: usize,
123 #[serde(default = "default_reset_token_ttl")]
124 pub reset_token_ttl_secs: u64,
125 #[serde(default = "default_reset_token_bytes")]
126 pub reset_token_bytes: usize,
127 #[serde(default = "default_cookie_prefix")]
128 pub cookie_name_prefix: String,
129 #[serde(default = "default_true_bool")]
130 pub cookie_secure: bool,
131 #[serde(default = "default_same_site")]
132 pub cookie_same_site: String,
133 #[serde(default)]
134 pub cookie_domain: Option<String>,
135 #[serde(default = "default_oauth_state_ttl")]
136 pub oauth_state_ttl_secs: u64,
137 #[serde(default = "default_email_rate_limit")]
138 pub email_code_rate_limit_secs: u64,
139 #[serde(default = "default_max_verify_attempts")]
140 pub max_verify_attempts: u32,
141 #[serde(default)]
142 pub auto_register_agreements: Vec<String>,
143 #[serde(default)]
144 pub oauth: OAuthConfig,
145 #[serde(default)]
146 pub agreements: AgreementsConfig,
147}
148
149fn default_verify_code_ttl() -> u64 {
150 600
151}
152fn default_verify_code_len() -> usize {
153 6
154}
155fn default_reset_token_ttl() -> u64 {
156 1800
157}
158fn default_reset_token_bytes() -> usize {
159 32
160}
161fn default_cookie_prefix() -> String {
162 "auth_".to_string()
163}
164fn default_true_bool() -> bool {
165 true
166}
167fn default_same_site() -> String {
168 "Strict".to_string()
169}
170fn default_oauth_state_ttl() -> u64 {
171 300
172}
173fn default_email_rate_limit() -> u64 {
174 60
175}
176fn default_max_verify_attempts() -> u32 {
177 5
178}
179
180impl Default for AuthConfig {
181 fn default() -> Self {
182 Self {
183 password_policy: PasswordPolicy::default(),
184 verify_code_ttl_secs: default_verify_code_ttl(),
185 verify_code_len: default_verify_code_len(),
186 reset_token_ttl_secs: default_reset_token_ttl(),
187 reset_token_bytes: default_reset_token_bytes(),
188 cookie_name_prefix: default_cookie_prefix(),
189 cookie_secure: default_true_bool(),
190 cookie_same_site: default_same_site(),
191 cookie_domain: None,
192 oauth_state_ttl_secs: default_oauth_state_ttl(),
193 email_code_rate_limit_secs: default_email_rate_limit(),
194 max_verify_attempts: default_max_verify_attempts(),
195 auto_register_agreements: Vec::new(),
196 oauth: OAuthConfig::default(),
197 agreements: AgreementsConfig::default(),
198 }
199 }
200}
201
202#[allow(dead_code)]
203pub type SharedAuthConfig = Arc<AuthConfig>;
No newline at end of file
Added lib/config/src/cache.rs lib/config/src/cache.rs +63 −0 View lib/config/src/cache.rs
@@ -0,0 +1,63 @@
1use lombok::{Builder, Getter, Setter};
2
3#[derive(
4 serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, Getter, Setter, Builder,
5)]
6#[serde(default)]
7pub struct AppCacheSettings {
8 pub enabled: bool,
9 pub ttl_seconds: u64,
10 pub max_capacity: usize,
11 pub max_size: usize,
12 pub enable_disk: bool,
13 pub disk_path: Option<String>,
14 pub disk_cache_max_size: usize,
15 pub disk_cache_max_capacity: usize,
16}
17
18impl Default for AppCacheSettings {
19 fn default() -> Self {
20 Self {
21 enabled: true,
22 ttl_seconds: 300,
23 max_capacity: 10_000,
24 max_size: 50_000_000,
25 enable_disk: false,
26 disk_path: None,
27 disk_cache_max_size: 200_000_000,
28 disk_cache_max_capacity: 100_000,
29 }
30 }
31}
32
33#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, Getter, Setter, Builder)]
34#[serde(default)]
35pub struct AppRedisSettings {
36 pub urls: Vec<String>,
37 pub mode: AppRedisMode,
38 pub connection_timeout_ms: Option<u64>,
39 pub response_timeout_ms: Option<u64>,
40 pub password: Option<String>,
41 pub database: Option<i64>,
42}
43
44#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, Default)]
45pub enum AppRedisMode {
46 #[default]
47 Single,
48 Cluster,
49 Sentinel,
50}
51
52impl Default for AppRedisSettings {
53 fn default() -> Self {
54 Self {
55 urls: vec!["redis://127.0.0.1:6379".to_string()],
56 mode: AppRedisMode::default(),
57 connection_timeout_ms: Some(2_000),
58 response_timeout_ms: Some(5_000),
59 password: None,
60 database: Some(0),
61 }
62 }
63}
No newline at end of file
Added lib/config/src/chat.rs lib/config/src/chat.rs +103 −0 View lib/config/src/chat.rs
@@ -0,0 +1,103 @@
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4#[serde(default, deny_unknown_fields)]
5pub struct AppChatSettings {
6 pub enabled: bool,
7 pub topic_prefix: String,
8 pub room_group_id: String,
9 pub user_group_id: String,
10 pub user_view_group_id: String,
11 pub dedup_ttl_seconds: u64,
12 pub idempotency_ttl_seconds: u64,
13 pub upload_ttl_seconds: u64,
14 pub presence_ttl_seconds: u64,
15 pub unread_ttl_seconds: u64,
16}
17
18impl Default for AppChatSettings {
19 fn default() -> Self {
20 Self {
21 enabled: true,
22 topic_prefix: "chat".to_string(),
23 room_group_id: "chat.room.v1".to_string(),
24 user_group_id: "chat.user.v1".to_string(),
25 user_view_group_id: "chat.user.view.v1".to_string(),
26 dedup_ttl_seconds: 24 * 3600,
27 idempotency_ttl_seconds: 24 * 3600,
28 upload_ttl_seconds: 3600,
29 presence_ttl_seconds: 120,
30 unread_ttl_seconds: 30 * 24 * 3600,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(default, deny_unknown_fields)]
37pub struct GatewaySettings {
38 pub ws_heartbeat_interval_seconds: u64,
39 pub ws_idle_timeout_seconds: u64,
40 pub ws_auth_timeout_seconds: u64,
41 pub max_connections_per_user: usize,
42 pub subscriber_id: String,
43 pub topic_prefix: String,
44 pub upload_presign_ttl_seconds: u64,
45 pub typing_debounce_seconds: u64,
46}
47
48impl Default for GatewaySettings {
49 fn default() -> Self {
50 Self {
51 ws_heartbeat_interval_seconds: 30,
52 ws_idle_timeout_seconds: 120,
53 ws_auth_timeout_seconds: 10,
54 max_connections_per_user: 8,
55 subscriber_id: "chat.gateway".to_string(),
56 topic_prefix: "chat".to_string(),
57 upload_presign_ttl_seconds: 900,
58 typing_debounce_seconds: 2,
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn chat_settings_defaults() {
69 let s = AppChatSettings::default();
70 assert!(s.enabled);
71 assert_eq!(s.topic_prefix, "chat");
72 assert_eq!(s.room_group_id, "chat.room.v1");
73 assert_eq!(s.user_group_id, "chat.user.v1");
74 assert_eq!(s.user_view_group_id, "chat.user.view.v1");
75 assert_eq!(s.dedup_ttl_seconds, 24 * 3600);
76 assert_eq!(s.idempotency_ttl_seconds, 24 * 3600);
77 assert_eq!(s.upload_ttl_seconds, 3600);
78 assert_eq!(s.presence_ttl_seconds, 120);
79 assert_eq!(s.unread_ttl_seconds, 30 * 24 * 3600);
80 }
81
82 #[test]
83 fn gateway_settings_defaults_sane() {
84 let s = GatewaySettings::default();
85 assert_eq!(s.ws_heartbeat_interval_seconds, 30);
86 assert_eq!(s.ws_idle_timeout_seconds, 120);
87 assert_eq!(s.ws_auth_timeout_seconds, 10);
88 assert_eq!(s.max_connections_per_user, 8);
89 assert_eq!(s.subscriber_id, "chat.gateway");
90 assert_eq!(s.topic_prefix, "chat");
91 assert_eq!(s.upload_presign_ttl_seconds, 900);
92 assert_eq!(s.typing_debounce_seconds, 2);
93 }
94
95 #[test]
96 fn gateway_settings_serde_roundtrip() {
97 let s = GatewaySettings::default();
98 let json = serde_json::to_string(&s).unwrap();
99 let back: GatewaySettings = serde_json::from_str(&json).unwrap();
100 assert_eq!(back.topic_prefix, s.topic_prefix);
101 assert_eq!(back.max_connections_per_user, s.max_connections_per_user);
102 }
103}
No newline at end of file
Added lib/config/src/db.rs lib/config/src/db.rs +55 −0 View lib/config/src/db.rs
@@ -0,0 +1,55 @@
1use lombok::{Builder, Getter, Setter};
2
3#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, Getter, Setter, Builder)]
4#[serde(default)]
5pub struct AppDatabaseSettings {
6 pub primary: AppDatabaseConnConfig,
7 pub replicas: Vec<AppDatabaseConnConfig>,
8 pub max_connections_total: u32,
9 pub query_timeout: u64,
10 pub auto_migrate: bool,
11 pub sqlx_logging: bool,
12 pub slow_query_threshold_ms: u64,
13}
14
15#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, Getter, Setter, Builder)]
16#[serde(default)]
17pub struct AppDatabaseConnConfig {
18 pub url: String,
19 pub max_connections: u32,
20 pub min_connections: u32,
21 pub max_lifetime: Option<u64>,
22 pub idle_timeout: Option<u64>,
23 pub acquire_timeout: Option<u64>,
24 pub schema: Option<String>,
25 pub application_name: Option<String>,
26}
27
28impl Default for AppDatabaseSettings {
29 fn default() -> Self {
30 Self {
31 primary: AppDatabaseConnConfig::default(),
32 replicas: vec![],
33 max_connections_total: 100,
34 query_timeout: 30,
35 auto_migrate: true,
36 sqlx_logging: false,
37 slow_query_threshold_ms: 500,
38 }
39 }
40}
41
42impl Default for AppDatabaseConnConfig {
43 fn default() -> Self {
44 Self {
45 url: String::new(),
46 max_connections: 20,
47 min_connections: 2,
48 max_lifetime: Some(1800),
49 idle_timeout: Some(600),
50 acquire_timeout: Some(5),
51 schema: None,
52 application_name: Some("gitdataai".to_string()),
53 }
54 }
55}
No newline at end of file
Added lib/config/src/duration.rs lib/config/src/duration.rs +176 −0 View lib/config/src/duration.rs
@@ -0,0 +1,176 @@
1use serde::{Deserialize, Deserializer};
2use std::time::Duration as StdDuration;
3use time::Duration as TimeDuration;
4
5fn de_error<E: serde::de::Error>(msg: impl std::fmt::Display) -> E {
6 E::custom(msg)
7}
8
9fn parse_native_time(s: &str) -> Result<TimeDuration, String> {
10 let (seconds, nanos) = s
11 .split_once('.')
12 .ok_or_else(|| format!("expected 'seconds.nanoseconds', got {s:?}"))?;
13 let seconds: i64 = seconds
14 .parse()
15 .map_err(|_| format!("invalid seconds in {s:?}"))?;
16 let nanos: i64 = nanos
17 .parse()
18 .map_err(|_| format!("invalid nanoseconds in {s:?}"))?;
19 Ok(TimeDuration::seconds(seconds) + TimeDuration::nanoseconds(nanos))
20}
21
22fn parse_human_time(s: &str) -> Result<TimeDuration, String> {
23 let s = s.trim();
24 if s.is_empty() {
25 return Err("empty duration string".to_string());
26 }
27 if let Ok(secs) = s.parse::<i64>() {
28 return Ok(TimeDuration::seconds(secs));
29 }
30 let mut total = TimeDuration::ZERO;
31 let mut buf = String::new();
32 let mut any = false;
33 for ch in s.chars() {
34 if ch.is_ascii_digit() || ch == '-' {
35 buf.push(ch);
36 } else {
37 let value: i64 = buf
38 .parse()
39 .map_err(|_| format!("invalid number in duration {s:?}"))?;
40 match ch {
41 's' => total += TimeDuration::seconds(value),
42 'm' => total += TimeDuration::minutes(value),
43 'h' => total += TimeDuration::hours(value),
44 'd' => total += TimeDuration::days(value),
45 other => return Err(format!("unknown duration unit {other:?} in {s:?}")),
46 }
47 buf.clear();
48 any = true;
49 }
50 }
51 if !buf.is_empty() {
52 return Err(format!("trailing number without unit in {s:?}"));
53 }
54 if !any {
55 return Err(format!("cannot parse duration {s:?}"));
56 }
57 Ok(total)
58}
59
60pub fn deserialize_time_duration<'de, D>(d: D) -> Result<TimeDuration, D::Error>
61where
62 D: Deserializer<'de>,
63{
64 #[derive(Deserialize)]
65 #[serde(untagged)]
66 enum Repr {
67 Secs(i64),
68 SecsF64(f64),
69 Text(String),
70 }
71 match Repr::deserialize(d)? {
72 Repr::Secs(secs) => Ok(TimeDuration::seconds(secs)),
73 Repr::SecsF64(secs) => Ok(TimeDuration::seconds_f64(secs)),
74 Repr::Text(t) if t.contains('.') => parse_native_time(&t).map_err(de_error),
75 Repr::Text(t) => parse_human_time(&t).map_err(de_error),
76 }
77}
78
79pub fn deserialize_std_duration<'de, D>(d: D) -> Result<StdDuration, D::Error>
80where
81 D: Deserializer<'de>,
82{
83 #[derive(Deserialize)]
84 #[serde(untagged)]
85 enum Repr {
86 Secs(u64),
87 Struct { secs: u64, nanos: u32 },
88 }
89 match Repr::deserialize(d)? {
90 Repr::Secs(secs) => Ok(StdDuration::from_secs(secs)),
91 Repr::Struct { secs, nanos } => Ok(StdDuration::new(secs, nanos)),
92 }
93}
94
95pub fn deserialize_opt_std_duration<'de, D>(d: D) -> Result<Option<StdDuration>, D::Error>
96where
97 D: Deserializer<'de>,
98{
99 #[derive(Deserialize)]
100 #[serde(untagged)]
101 enum Repr {
102 Secs(u64),
103 Struct { secs: u64, nanos: u32 },
104 }
105 match Option::<Repr>::deserialize(d)? {
106 Some(Repr::Secs(secs)) => Ok(Some(StdDuration::from_secs(secs))),
107 Some(Repr::Struct { secs, nanos }) => Ok(Some(StdDuration::new(secs, nanos))),
108 None => Ok(None),
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use serde::Deserialize;
116
117 #[derive(Deserialize)]
118 struct TimeWrap {
119 #[serde(deserialize_with = "deserialize_time_duration")]
120 d: TimeDuration,
121 }
122
123 #[derive(Deserialize)]
124 struct StdWrap {
125 #[serde(deserialize_with = "deserialize_std_duration")]
126 d: StdDuration,
127 }
128
129 #[derive(Deserialize)]
130 struct OptStdWrap {
131 #[serde(default, deserialize_with = "deserialize_opt_std_duration")]
132 d: Option<StdDuration>,
133 }
134
135 #[test]
136 fn time_duration_from_bare_seconds() {
137 let w: TimeWrap = serde_json::from_str(r#"{"d":900}"#).unwrap();
138 assert_eq!(w.d, TimeDuration::seconds(900));
139 }
140
141 #[test]
142 fn time_duration_from_human_string() {
143 let w: TimeWrap = serde_json::from_str(r#"{"d":"1h30m"}"#).unwrap();
144 assert_eq!(w.d, TimeDuration::hours(1) + TimeDuration::minutes(30));
145 }
146
147 #[test]
148 fn time_duration_from_native_string() {
149 let w: TimeWrap = serde_json::from_str(r#"{"d":"900.000000000"}"#).unwrap();
150 assert_eq!(w.d, TimeDuration::seconds(900));
151 }
152
153 #[test]
154 fn std_duration_from_bare_seconds() {
155 let w: StdWrap = serde_json::from_str(r#"{"d":30}"#).unwrap();
156 assert_eq!(w.d, StdDuration::from_secs(30));
157 }
158
159 #[test]
160 fn std_duration_from_native_struct() {
161 let w: StdWrap = serde_json::from_str(r#"{"d":{"secs":30,"nanos":0}}"#).unwrap();
162 assert_eq!(w.d, StdDuration::from_secs(30));
163 }
164
165 #[test]
166 fn opt_std_duration_none_when_absent() {
167 let w: OptStdWrap = serde_json::from_str(r#"{}"#).unwrap();
168 assert_eq!(w.d, None);
169 }
170
171 #[test]
172 fn opt_std_duration_some_from_seconds() {
173 let w: OptStdWrap = serde_json::from_str(r#"{"d":300}"#).unwrap();
174 assert_eq!(w.d, Some(StdDuration::from_secs(300)));
175 }
176}
No newline at end of file
Added lib/config/src/email.rs lib/config/src/email.rs +75 −0 View lib/config/src/email.rs
@@ -0,0 +1,75 @@
1use lombok::{Builder, Getter, Setter};
2
3#[derive(
4 serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, Getter, Setter, Builder,
5)]
6#[serde(default)]
7pub struct AppEmailSettings {
8 pub enabled: bool,
9 pub smtp_host: String,
10 pub smtp_port: u16,
11 pub smtp_username: Option<String>,
12 pub smtp_password: Option<String>,
13 pub from_address: String,
14 pub from_name: Option<String>,
15 pub connection_timeout_ms: u64,
16 pub response_timeout_ms: u64,
17 pub template_dir: Option<String>,
18 pub default_locale: String,
19 pub max_attachment_bytes: usize,
20 pub subscriber_group_id: String,
21 pub auto_offset_reset: String,
22}
23
24impl Default for AppEmailSettings {
25 fn default() -> Self {
26 Self {
27 enabled: true,
28 smtp_host: "127.0.0.1".to_string(),
29 smtp_port: 587,
30 smtp_username: None,
31 smtp_password: None,
32 from_address: "no-reply@example.com".to_string(),
33 from_name: Some("No Reply".to_string()),
34 connection_timeout_ms: 5_000,
35 response_timeout_ms: 10_000,
36 template_dir: None,
37 default_locale: "en".to_string(),
38 max_attachment_bytes: 25 * 1024 * 1024,
39 subscriber_group_id: "email-service".to_string(),
40 auto_offset_reset: "earliest".to_string(),
41 }
42 }
43}
44
45impl AppEmailSettings {
46 pub fn is_valid(&self) -> Result<(), String> {
47 if !self.enabled {
48 return Ok(());
49 }
50 if self.smtp_host.trim().is_empty() {
51 return Err("smtp_host must not be empty when enabled".to_string());
52 }
53 if self.smtp_port == 0 {
54 return Err("smtp_port must be > 0".to_string());
55 }
56 if self.from_address.trim().is_empty() {
57 return Err("from_address must not be empty".to_string());
58 }
59 if self.max_attachment_bytes == 0 {
60 return Err("max_attachment_bytes must be > 0".to_string());
61 }
62 match self.auto_offset_reset.as_str() {
63 "earliest" | "latest" | "none" => {}
64 other => {
65 return Err(format!(
66 "auto_offset_reset must be earliest|latest|none, got {other}"
67 ));
68 }
69 }
70 if self.subscriber_group_id.is_empty() {
71 return Err("subscriber_group_id must not be empty".to_string());
72 }
73 Ok(())
74 }
75}
No newline at end of file
Added lib/config/src/error.rs lib/config/src/error.rs +11 −0 View lib/config/src/error.rs
@@ -0,0 +1,11 @@
1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum ConfigError {
5 #[error("config load failed: {0}")]
6 Load(#[from] config_rs::ConfigError),
7 #[error("invalid configuration: {0}")]
8 Validation(String),
9}
10
11pub type ConfigResult<T> = Result<T, ConfigError>;
No newline at end of file
Added lib/config/src/git.rs lib/config/src/git.rs +116 −0 View lib/config/src/git.rs
@@ -0,0 +1,116 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use crate::duration::{deserialize_opt_std_duration, deserialize_std_duration};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9pub struct SmartConfig {
10 pub git_storage_root: String,
11 pub lfs_storage_root: String,
12 pub object_cache_prefix: String,
13 pub object_cache_ttl: u64,
14}
15
16impl Default for SmartConfig {
17 fn default() -> Self {
18 Self {
19 git_storage_root: "/var/lib/repo/git".to_string(),
20 lfs_storage_root: "/var/lib/repo/lfs".to_string(),
21 object_cache_prefix: "gitsmart".to_string(),
22 object_cache_ttl: 300,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(default)]
29pub struct GitSshConfig {
30 pub bind_addr: String,
31 pub port: u16,
32 pub host_key_path: String,
33 pub git_storage_root: String,
34 pub max_sessions: usize,
35 pub banner: Option<String>,
36 #[serde(default, deserialize_with = "deserialize_std_duration")]
37 pub auth_rejection_time: Duration,
38 #[serde(default, deserialize_with = "deserialize_opt_std_duration")]
39 pub inactivity_timeout: Option<Duration>,
40 #[serde(default, deserialize_with = "deserialize_std_duration")]
41 pub keepalive_interval: Duration,
42 pub keepalive_max: u32,
43}
44
45impl Default for GitSshConfig {
46 fn default() -> Self {
47 Self {
48 bind_addr: "0.0.0.0".to_string(),
49 port: 2222,
50 host_key_path: "/etc/gitssh/ssh_host_ed25519_key".to_string(),
51 git_storage_root: "/var/lib/repo/git".to_string(),
52 max_sessions: 32,
53 banner: None,
54 auth_rejection_time: Duration::from_secs(1),
55 inactivity_timeout: Some(Duration::from_secs(300)),
56 keepalive_interval: Duration::from_secs(30),
57 keepalive_max: 3,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(default)]
64pub struct GitAuthConfig {
65 pub allow_anonymous_read: bool,
66}
67
68impl Default for GitAuthConfig {
69 fn default() -> Self {
70 Self {
71 allow_anonymous_read: true,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(default)]
78pub struct SyncSettings {
79 pub queue_topic: String,
80 pub dlq_topic: String,
81 pub group_id: String,
82 pub concurrency: usize,
83 pub lock_ttl_secs: u64,
84 pub max_commits_per_sync: usize,
85 pub max_lock_retries: u32,
86 pub language_timeout_secs: u64,
87 pub max_languages: usize,
88}
89
90impl Default for SyncSettings {
91 fn default() -> Self {
92 Self {
93 queue_topic: "gta.git.sync".to_string(),
94 dlq_topic: "gta.git.sync.dlq".to_string(),
95 group_id: "git-sync-workers".to_string(),
96 concurrency: 4,
97 lock_ttl_secs: 300,
98 max_commits_per_sync: 10_000,
99 max_lock_retries: 5,
100 language_timeout_secs: 30,
101 max_languages: 50,
102 }
103 }
104}
105
106impl SyncSettings {
107 #[must_use]
108 pub fn lock_ttl(&self) -> Duration {
109 Duration::from_secs(self.lock_ttl_secs)
110 }
111
112 #[must_use]
113 pub fn language_timeout(&self) -> Duration {
114 Duration::from_secs(self.language_timeout_secs)
115 }
116}
No newline at end of file
Added lib/config/src/issues.rs lib/config/src/issues.rs +107 −0 View lib/config/src/issues.rs
@@ -0,0 +1,107 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct IssueConfig {
7 #[serde(default = "default_max_pagination")]
8 pub max_pagination: u64,
9 #[serde(default = "default_default_pagination")]
10 pub default_pagination: u64,
11 #[serde(default = "default_max_title_len")]
12 pub max_title_len: usize,
13 #[serde(default = "default_max_body_len")]
14 pub max_body_len: usize,
15 #[serde(default = "default_max_collection_name_len")]
16 pub max_collection_name_len: usize,
17 #[serde(default = "default_max_label_name_len")]
18 pub max_label_name_len: usize,
19 #[serde(default = "default_max_label_color_len")]
20 pub max_label_color_len: usize,
21 #[serde(default = "default_max_milestone_title_len")]
22 pub max_milestone_title_len: usize,
23 #[serde(default = "default_max_milestone_description_len")]
24 pub max_milestone_description_len: usize,
25 #[serde(default = "default_max_comment_len")]
26 pub max_comment_len: usize,
27 #[serde(default = "default_default_collection_name")]
28 pub default_collection_name: String,
29 #[serde(default = "default_max_reaction_emoji_len")]
30 pub max_reaction_emoji_len: usize,
31 #[serde(default = "default_max_attachment_size_bytes")]
32 pub max_attachment_size_bytes: i64,
33 #[serde(default = "default_max_dependency_label_len")]
34 pub max_dependency_label_len: usize,
35 #[serde(default = "default_max_event_content_len")]
36 pub max_event_content_len: usize,
37}
38
39fn default_max_pagination() -> u64 {
40 100
41}
42fn default_default_pagination() -> u64 {
43 20
44}
45fn default_max_title_len() -> usize {
46 255
47}
48fn default_max_body_len() -> usize {
49 65_535
50}
51fn default_max_collection_name_len() -> usize {
52 100
53}
54fn default_max_label_name_len() -> usize {
55 50
56}
57fn default_max_label_color_len() -> usize {
58 7
59}
60fn default_max_milestone_title_len() -> usize {
61 100
62}
63fn default_max_milestone_description_len() -> usize {
64 1024
65}
66fn default_max_comment_len() -> usize {
67 65_535
68}
69fn default_default_collection_name() -> String {
70 "default".to_string()
71}
72fn default_max_reaction_emoji_len() -> usize {
73 32
74}
75fn default_max_attachment_size_bytes() -> i64 {
76 10 * 1024 * 1024
77}
78fn default_max_dependency_label_len() -> usize {
79 100
80}
81fn default_max_event_content_len() -> usize {
82 4096
83}
84
85impl Default for IssueConfig {
86 fn default() -> Self {
87 Self {
88 max_pagination: default_max_pagination(),
89 default_pagination: default_default_pagination(),
90 max_title_len: default_max_title_len(),
91 max_body_len: default_max_body_len(),
92 max_collection_name_len: default_max_collection_name_len(),
93 max_label_name_len: default_max_label_name_len(),
94 max_label_color_len: default_max_label_color_len(),
95 max_milestone_title_len: default_max_milestone_title_len(),
96 max_milestone_description_len: default_max_milestone_description_len(),
97 max_comment_len: default_max_comment_len(),
98 default_collection_name: default_default_collection_name(),
99 max_reaction_emoji_len: default_max_reaction_emoji_len(),
100 max_attachment_size_bytes: default_max_attachment_size_bytes(),
101 max_dependency_label_len: default_max_dependency_label_len(),
102 max_event_content_len: default_max_event_content_len(),
103 }
104 }
105}
106
107pub type SharedIssueConfig = Arc<IssueConfig>;
No newline at end of file
Added lib/config/src/jwt.rs lib/config/src/jwt.rs +39 −0 View lib/config/src/jwt.rs
@@ -0,0 +1,39 @@
1use serde::{Deserialize, Serialize};
2use time::Duration;
3
4use crate::duration::deserialize_time_duration;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct JwtSettings {
8 #[serde(default)]
9 pub issuer: String,
10 #[serde(default)]
11 pub audience: String,
12 #[serde(default, deserialize_with = "deserialize_time_duration")]
13 pub access_ttl: Duration,
14 #[serde(default, deserialize_with = "deserialize_time_duration")]
15 pub refresh_ttl: Duration,
16 #[serde(default, deserialize_with = "deserialize_time_duration")]
17 pub grace_window: Duration,
18 #[serde(default)]
19 pub clock_skew_seconds: i64,
20 #[serde(default, deserialize_with = "deserialize_time_duration")]
21 pub default_grace_duration: Duration,
22 #[serde(default)]
23 pub default_key_pair_algorithm: String,
24}
25
26impl Default for JwtSettings {
27 fn default() -> Self {
28 Self {
29 issuer: "untitled".to_string(),
30 audience: "untitled-api".to_string(),
31 access_ttl: Duration::minutes(15),
32 refresh_ttl: Duration::days(30),
33 grace_window: Duration::minutes(5),
34 clock_skew_seconds: 60,
35 default_grace_duration: Duration::days(7),
36 default_key_pair_algorithm: "EdDSA".to_string(),
37 }
38 }
39}
No newline at end of file
Added lib/config/src/lib.rs lib/config/src/lib.rs +26 −0 View lib/config/src/lib.rs
@@ -0,0 +1,26 @@
1pub mod app;
2pub mod auth;
3pub mod cache;
4pub mod chat;
5pub mod db;
6pub mod duration;
7pub mod email;
8pub mod error;
9pub mod git;
10pub mod issues;
11pub mod jwt;
12pub mod loader;
13pub mod payment;
14pub mod pow;
15pub mod project;
16pub mod queue;
17pub mod rate_limit;
18pub mod repo;
19pub mod search;
20pub mod space;
21pub mod storage;
22pub mod user;
23
24pub use app::AppConfig;
25pub use error::{ConfigError, ConfigResult};
26pub use loader::{ConfigLoader, load};
No newline at end of file
Added lib/config/src/loader.rs lib/config/src/loader.rs +110 −0 View lib/config/src/loader.rs
@@ -0,0 +1,110 @@
1use std::path::PathBuf;
2
3use crate::app::AppConfig;
4use crate::error::{ConfigError, ConfigResult};
5
6#[derive(Debug, Clone)]
7pub struct ConfigLoader {
8 config_file: Option<PathBuf>,
9 local_config_file: Option<PathBuf>,
10 env_prefix: String,
11 use_env: bool,
12 use_dotenv: bool,
13 validate: bool,
14}
15
16impl Default for ConfigLoader {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl ConfigLoader {
23 pub fn new() -> Self {
24 Self {
25 config_file: Some(PathBuf::from("config.toml")),
26 local_config_file: Some(PathBuf::from("config.local.toml")),
27 env_prefix: "APP".to_string(),
28 use_env: true,
29 use_dotenv: true,
30 validate: true,
31 }
32 }
33
34 pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
35 self.config_file = Some(path.into());
36 self
37 }
38
39 pub fn local_config_file(mut self, path: impl Into<PathBuf>) -> Self {
40 self.local_config_file = Some(path.into());
41 self
42 }
43
44 pub fn no_config_file(mut self) -> Self {
45 self.config_file = None;
46 self
47 }
48
49 pub fn no_local_config_file(mut self) -> Self {
50 self.local_config_file = None;
51 self
52 }
53
54 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
55 self.env_prefix = prefix.into();
56 self
57 }
58
59 pub fn no_env(mut self) -> Self {
60 self.use_env = false;
61 self
62 }
63
64 pub fn no_dotenv(mut self) -> Self {
65 self.use_dotenv = false;
66 self
67 }
68
69 pub fn validate(mut self, on: bool) -> Self {
70 self.validate = on;
71 self
72 }
73
74 pub fn load(self) -> ConfigResult<AppConfig> {
75 if self.use_dotenv {
76 let _ = dotenvy::dotenv();
77 }
78
79 let mut builder = config_rs::Config::builder();
80 if let Some(path) = self.config_file {
81 builder = builder.add_source(
82 config_rs::File::with_name(&path.to_string_lossy()).required(false),
83 );
84 }
85 if let Some(path) = self.local_config_file {
86 builder = builder.add_source(
87 config_rs::File::with_name(&path.to_string_lossy()).required(false),
88 );
89 }
90 if self.use_env {
91 builder = builder.add_source(
92 config_rs::Environment::with_prefix(&self.env_prefix)
93 .prefix_separator("_")
94 .separator("__")
95 .try_parsing(true),
96 );
97 }
98
99 let conf = builder.build()?;
100 let app: AppConfig = conf.try_deserialize()?;
101 if self.validate {
102 app.validate().map_err(ConfigError::Validation)?;
103 }
104 Ok(app)
105 }
106}
107
108pub fn load() -> ConfigResult<AppConfig> {
109 ConfigLoader::new().load()
110}
No newline at end of file
Added lib/config/src/payment.rs lib/config/src/payment.rs +45 −0 View lib/config/src/payment.rs
@@ -0,0 +1,45 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct PaymentConfig {
7 #[serde(default = "default_currency")]
8 pub currency: String,
9 #[serde(default = "default_max_pagination")]
10 pub max_pagination: u64,
11 #[serde(default)]
12 pub airwallex: AirwallexConfig,
13}
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct AirwallexConfig {
17 #[serde(default)]
18 pub base_url: String,
19 #[serde(default)]
20 pub client_id: String,
21 #[serde(default)]
22 pub api_key: String,
23 #[serde(default)]
24 pub webhook_secret: String,
25}
26
27pub type SharedPaymentConfig = Arc<PaymentConfig>;
28
29impl Default for PaymentConfig {
30 fn default() -> Self {
31 Self {
32 currency: default_currency(),
33 max_pagination: default_max_pagination(),
34 airwallex: AirwallexConfig::default(),
35 }
36 }
37}
38
39fn default_currency() -> String {
40 "CNY".to_string()
41}
42
43fn default_max_pagination() -> u64 {
44 100
45}
No newline at end of file
Added lib/config/src/pow.rs lib/config/src/pow.rs +39 −0 View lib/config/src/pow.rs
@@ -0,0 +1,39 @@
1use serde::{Deserialize, Serialize};
2use time::Duration;
3
4use crate::duration::deserialize_time_duration;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PowSettings {
8 #[serde(default)]
9 pub hmac_secret: String,
10 #[serde(default)]
11 pub algorithm: String,
12 #[serde(default)]
13 pub cost: u32,
14 #[serde(default)]
15 pub key_length: usize,
16 #[serde(default)]
17 pub key_prefix: String,
18 #[serde(default)]
19 pub memory_cost: Option<u32>,
20 #[serde(default)]
21 pub parallelism: Option<u32>,
22 #[serde(default, deserialize_with = "deserialize_time_duration")]
23 pub challenge_ttl: Duration,
24}
25
26impl Default for PowSettings {
27 fn default() -> Self {
28 Self {
29 hmac_secret: String::new(),
30 algorithm: "PBKDF2/SHA-256".to_string(),
31 cost: 100_000,
32 key_length: 32,
33 key_prefix: "00".to_string(),
34 memory_cost: None,
35 parallelism: None,
36 challenge_ttl: Duration::minutes(5),
37 }
38 }
39}
No newline at end of file
Added lib/config/src/project.rs lib/config/src/project.rs +27 −0 View lib/config/src/project.rs
@@ -0,0 +1,27 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(default)]
7pub struct ProjectConfig {
8 pub cache_ttl_project_secs: u64,
9 pub cache_ttl_member_role_secs: u64,
10 pub cache_ttl_favorites_secs: u64,
11 pub default_page_size: u64,
12 pub max_page_size: u64,
13}
14
15impl Default for ProjectConfig {
16 fn default() -> Self {
17 Self {
18 cache_ttl_project_secs: 300,
19 cache_ttl_member_role_secs: 60,
20 cache_ttl_favorites_secs: 600,
21 default_page_size: 20,
22 max_page_size: 100,
23 }
24 }
25}
26
27pub type SharedProjectConfig = Arc<ProjectConfig>;
No newline at end of file
Added lib/config/src/queue.rs lib/config/src/queue.rs +76 −0 View lib/config/src/queue.rs
@@ -0,0 +1,76 @@
1use std::collections::HashMap;
2
3use lombok::{Builder, Getter, Setter};
4
5#[derive(
6 serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, Getter, Setter, Builder,
7)]
8#[serde(default)]
9pub struct AppQueueSettings {
10 pub enabled: bool,
11 pub brokers: String,
12 pub client_id: String,
13 pub topic_prefix: String,
14 pub default_group_id: String,
15 pub message_timeout_ms: u32,
16 pub session_timeout_ms: u32,
17 pub auto_offset_reset: String,
18 pub producer_compression: String,
19 pub producer_acks: String,
20 pub publish_queue_timeout_ms: u32,
21 pub subscribe_max_retries: u32,
22 pub subscribe_graceful_shutdown_ms: u64,
23 pub additional_properties: HashMap<String, String>,
24}
25
26impl Default for AppQueueSettings {
27 fn default() -> Self {
28 Self {
29 enabled: true,
30 brokers: "127.0.0.1:9092".to_string(),
31 client_id: "untitled".to_string(),
32 topic_prefix: "untitled".to_string(),
33 default_group_id: "untitled".to_string(),
34 message_timeout_ms: 5_000,
35 session_timeout_ms: 10_000,
36 auto_offset_reset: "earliest".to_string(),
37 producer_compression: "lz4".to_string(),
38 producer_acks: "all".to_string(),
39 publish_queue_timeout_ms: 1_000,
40 subscribe_max_retries: 3,
41 subscribe_graceful_shutdown_ms: 5_000,
42 additional_properties: HashMap::new(),
43 }
44 }
45}
46
47impl AppQueueSettings {
48 pub fn is_valid(&self) -> Result<(), String> {
49 if !self.enabled {
50 return Ok(());
51 }
52 if self.brokers.trim().is_empty() {
53 return Err("brokers must not be empty when enabled".to_string());
54 }
55 if self.topic_prefix.is_empty() {
56 return Err("topic_prefix must not be empty".to_string());
57 }
58 match self.auto_offset_reset.as_str() {
59 "earliest" | "latest" | "none" => {}
60 other => {
61 return Err(format!(
62 "auto_offset_reset must be one of earliest|latest|none, got {other}"
63 ));
64 }
65 }
66 match self.producer_acks.as_str() {
67 "0" | "1" | "all" => {}
68 other => return Err(format!("producer_acks must be one of 0|1|all, got {other}")),
69 }
70 match self.producer_compression.as_str() {
71 "none" | "gzip" | "snappy" | "lz4" | "zstd" => {}
72 other => return Err(format!("producer_compression invalid: {other}")),
73 }
74 Ok(())
75 }
76}
No newline at end of file
Added lib/config/src/rate_limit.rs lib/config/src/rate_limit.rs +34 −0 View lib/config/src/rate_limit.rs
@@ -0,0 +1,34 @@
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct RateLimitConfig {
5 #[serde(default)]
6 pub default_limit: u64,
7 #[serde(default)]
8 pub window_secs: i64,
9 #[serde(default)]
10 pub cookie_name: String,
11 #[serde(default)]
12 pub cookie_secure: bool,
13}
14
15impl Default for RateLimitConfig {
16 fn default() -> Self {
17 Self {
18 default_limit: 100,
19 window_secs: 60,
20 cookie_name: "rl_id".to_string(),
21 cookie_secure: true,
22 }
23 }
24}
25
26impl RateLimitConfig {
27 pub fn new(default_limit: u64, window_secs: i64) -> Self {
28 Self {
29 default_limit,
30 window_secs,
31 ..Self::default()
32 }
33 }
34}
No newline at end of file
Added lib/config/src/repo.rs lib/config/src/repo.rs +119 −0 View lib/config/src/repo.rs
@@ -0,0 +1,119 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct RepoConfig {
7 #[serde(default = "default_max_pagination")]
8 pub max_pagination: u64,
9 #[serde(default = "default_default_pagination")]
10 pub default_pagination: u64,
11 #[serde(default = "default_repo_name_len")]
12 pub max_repo_name_len: usize,
13 #[serde(default = "default_description_len")]
14 pub max_description_len: usize,
15 #[serde(default = "default_website_len")]
16 pub max_website_len: usize,
17 #[serde(default = "default_topic_len")]
18 pub max_topic_len: usize,
19 #[serde(default = "default_branch_name_len")]
20 pub max_branch_name_len: usize,
21 #[serde(default = "default_tag_name_len")]
22 pub max_tag_name_len: usize,
23 #[serde(default = "default_release_body_len")]
24 pub max_release_body_len: usize,
25 #[serde(default = "default_default_branch")]
26 pub default_branch: String,
27 #[serde(default = "default_object_format")]
28 pub default_object_format: String,
29 #[serde(default = "default_git_dir")]
30 pub git_storage_root: String,
31 #[serde(default = "default_lfs_dir")]
32 pub lfs_storage_root: String,
33 #[serde(default = "default_branch_protection_priority_max")]
34 pub branch_protection_priority_max: i64,
35 #[serde(default = "default_max_topics")]
36 pub max_topics_per_repo: usize,
37 #[serde(default = "default_pr_title_len")]
38 pub max_pr_title_len: usize,
39 #[serde(default = "default_pr_body_len")]
40 pub max_pr_body_len: usize,
41}
42
43fn default_max_pagination() -> u64 {
44 100
45}
46fn default_default_pagination() -> u64 {
47 20
48}
49fn default_repo_name_len() -> usize {
50 100
51}
52fn default_description_len() -> usize {
53 512
54}
55fn default_website_len() -> usize {
56 256
57}
58fn default_topic_len() -> usize {
59 35
60}
61fn default_branch_name_len() -> usize {
62 100
63}
64fn default_tag_name_len() -> usize {
65 100
66}
67fn default_release_body_len() -> usize {
68 65535
69}
70fn default_default_branch() -> String {
71 "main".to_string()
72}
73fn default_object_format() -> String {
74 "sha1".to_string()
75}
76fn default_git_dir() -> String {
77 "/var/lib/repo/git".to_string()
78}
79fn default_lfs_dir() -> String {
80 "/var/lib/repo/lfs".to_string()
81}
82fn default_branch_protection_priority_max() -> i64 {
83 10_000
84}
85fn default_max_topics() -> usize {
86 25
87}
88fn default_pr_title_len() -> usize {
89 255
90}
91fn default_pr_body_len() -> usize {
92 65535
93}
94
95impl Default for RepoConfig {
96 fn default() -> Self {
97 Self {
98 max_pagination: default_max_pagination(),
99 default_pagination: default_default_pagination(),
100 max_repo_name_len: default_repo_name_len(),
101 max_description_len: default_description_len(),
102 max_website_len: default_website_len(),
103 max_topic_len: default_topic_len(),
104 max_branch_name_len: default_branch_name_len(),
105 max_tag_name_len: default_tag_name_len(),
106 max_release_body_len: default_release_body_len(),
107 default_branch: default_default_branch(),
108 default_object_format: default_object_format(),
109 git_storage_root: default_git_dir(),
110 lfs_storage_root: default_lfs_dir(),
111 branch_protection_priority_max: default_branch_protection_priority_max(),
112 max_topics_per_repo: default_max_topics(),
113 max_pr_title_len: default_pr_title_len(),
114 max_pr_body_len: default_pr_body_len(),
115 }
116 }
117}
118
119pub type SharedRepoConfig = Arc<RepoConfig>;
No newline at end of file
Added lib/config/src/search.rs lib/config/src/search.rs +130 −0 View lib/config/src/search.rs
@@ -0,0 +1,130 @@
1use lombok::{Builder, Getter, Setter};
2
3#[derive(
4 serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, Getter, Setter, Builder,
5)]
6#[serde(default)]
7pub struct AppSearchSettings {
8 pub enabled: bool,
9 pub host: String,
10 /// Master / admin API key. `None` is allowed for local development.
11 pub api_key: Option<String>,
12 pub index_prefix: String,
13 pub default_primary_key: String,
14 pub batch_size: usize,
15 pub task_wait_timeout_ms: u64,
16 pub task_poll_interval_ms: u64,
17}
18
19impl Default for AppSearchSettings {
20 fn default() -> Self {
21 Self {
22 enabled: true,
23 host: "http://127.0.0.1:7700".to_string(),
24 api_key: None,
25 index_prefix: "untitled".to_string(),
26 default_primary_key: "id".to_string(),
27 batch_size: 1_000,
28 task_wait_timeout_ms: 30_000,
29 task_poll_interval_ms: 50,
30 }
31 }
32}
33
34impl AppSearchSettings {
35 pub fn is_valid(&self) -> Result<(), String> {
36 if !self.enabled {
37 return Ok(());
38 }
39 if self.host.trim().is_empty() {
40 return Err("host must not be empty when enabled".to_string());
41 }
42 if !self.host.starts_with("http://") && !self.host.starts_with("https://") {
43 return Err(format!(
44 "host must start with http:// or https://, got {}",
45 self.host
46 ));
47 }
48 if self.host.ends_with('/') {
49 return Err(format!("host must not end with '/', got {}", self.host));
50 }
51 if let Some(key) = &self.api_key
52 && key.trim().is_empty()
53 {
54 return Err("api_key must not be empty when provided".to_string());
55 }
56 if self.index_prefix.is_empty() {
57 return Err("index_prefix must not be empty".to_string());
58 }
59 if self.batch_size == 0 {
60 return Err("batch_size must be greater than 0".to_string());
61 }
62 Ok(())
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn default_config_is_valid() {
72 assert!(AppSearchSettings::default().is_valid().is_ok());
73 }
74
75 #[test]
76 fn disabled_tolerates_empty_host() {
77 let cfg = AppSearchSettings {
78 enabled: false,
79 host: "".into(),
80 ..Default::default()
81 };
82 assert!(cfg.is_valid().is_ok());
83 }
84
85 #[test]
86 fn empty_host_rejected() {
87 let cfg = AppSearchSettings {
88 enabled: true,
89 host: "".into(),
90 ..Default::default()
91 };
92 assert!(cfg.is_valid().is_err());
93 }
94
95 #[test]
96 fn host_without_scheme_rejected() {
97 let cfg = AppSearchSettings {
98 host: "127.0.0.1:7700".into(),
99 ..Default::default()
100 };
101 assert!(cfg.is_valid().is_err());
102 }
103
104 #[test]
105 fn host_trailing_slash_rejected() {
106 let cfg = AppSearchSettings {
107 host: "http://127.0.0.1:7700/".into(),
108 ..Default::default()
109 };
110 assert!(cfg.is_valid().is_err());
111 }
112
113 #[test]
114 fn empty_api_key_rejected() {
115 let cfg = AppSearchSettings {
116 api_key: Some("".into()),
117 ..Default::default()
118 };
119 assert!(cfg.is_valid().is_err());
120 }
121
122 #[test]
123 fn zero_batch_size_rejected() {
124 let cfg = AppSearchSettings {
125 batch_size: 0,
126 ..Default::default()
127 };
128 assert!(cfg.is_valid().is_err());
129 }
130}
No newline at end of file
Added lib/config/src/space.rs lib/config/src/space.rs +84 −0 View lib/config/src/space.rs
@@ -0,0 +1,84 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct SpaceConfig {
7 #[serde(default = "default_max_pagination")]
8 pub max_pagination: u64,
9 #[serde(default = "default_default_pagination")]
10 pub default_pagination: u64,
11 #[serde(default = "default_max_space_name_len")]
12 pub max_space_name_len: usize,
13 #[serde(default = "default_max_space_desc_len")]
14 pub max_space_desc_len: usize,
15 #[serde(default = "default_max_role_name_len")]
16 pub max_role_name_len: usize,
17 #[serde(default = "default_max_alias_name_len")]
18 pub max_alias_name_len: usize,
19 #[serde(default = "default_max_invite_email_len")]
20 pub max_invite_email_len: usize,
21 #[serde(default = "default_max_channel_name_len")]
22 pub max_channel_name_len: usize,
23 #[serde(default = "default_max_topic_len")]
24 pub max_topic_len: usize,
25 #[serde(default = "default_max_purpose_len")]
26 pub max_purpose_len: usize,
27 #[serde(default = "default_invite_expiry_hours")]
28 pub invite_expiry_hours: i64,
29}
30
31fn default_max_pagination() -> u64 {
32 100
33}
34fn default_default_pagination() -> u64 {
35 20
36}
37fn default_max_space_name_len() -> usize {
38 64
39}
40fn default_max_space_desc_len() -> usize {
41 512
42}
43fn default_max_role_name_len() -> usize {
44 64
45}
46fn default_max_alias_name_len() -> usize {
47 64
48}
49fn default_max_invite_email_len() -> usize {
50 320
51}
52fn default_max_channel_name_len() -> usize {
53 64
54}
55fn default_max_topic_len() -> usize {
56 256
57}
58fn default_max_purpose_len() -> usize {
59 512
60}
61fn default_invite_expiry_hours() -> i64 {
62 24
63}
64
65impl Default for SpaceConfig {
66 fn default() -> Self {
67 Self {
68 max_pagination: default_max_pagination(),
69 default_pagination: default_default_pagination(),
70 max_space_name_len: default_max_space_name_len(),
71 max_space_desc_len: default_max_space_desc_len(),
72 max_role_name_len: default_max_role_name_len(),
73 max_alias_name_len: default_max_alias_name_len(),
74 max_invite_email_len: default_max_invite_email_len(),
75 max_channel_name_len: default_max_channel_name_len(),
76 max_topic_len: default_max_topic_len(),
77 max_purpose_len: default_max_purpose_len(),
78 invite_expiry_hours: default_invite_expiry_hours(),
79 }
80 }
81}
82
83#[allow(dead_code)]
84pub type SharedSpaceConfig = Arc<SpaceConfig>;
No newline at end of file
Added lib/config/src/storage.rs lib/config/src/storage.rs +111 −0 View lib/config/src/storage.rs
@@ -0,0 +1,111 @@
1use std::path::PathBuf;
2
3use lombok::{Builder, Getter, Setter};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
6#[serde(rename_all = "lowercase")]
7pub enum BackendType {
8 #[default]
9 Local,
10 S3,
11}
12
13#[derive(
14 Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, Getter, Setter, Builder,
15)]
16#[serde(default)]
17pub struct StorageSettings {
18 backend: BackendType,
19 local: LocalStorageSettings,
20 s3: S3StorageSettings,
21}
22
23impl Default for StorageSettings {
24 fn default() -> Self {
25 Self {
26 backend: BackendType::Local,
27 local: LocalStorageSettings::default(),
28 s3: S3StorageSettings::default(),
29 }
30 }
31}
32
33#[derive(
34 Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, Getter, Setter, Builder,
35)]
36#[serde(default)]
37pub struct LocalStorageSettings {
38 root_dir: PathBuf,
39}
40
41impl Default for LocalStorageSettings {
42 fn default() -> Self {
43 Self {
44 root_dir: PathBuf::from("./storage"),
45 }
46 }
47}
48
49#[derive(
50 Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, Getter, Setter, Builder,
51)]
52#[serde(default)]
53pub struct S3StorageSettings {
54 endpoint: Option<String>,
55 region: String,
56 bucket: String,
57 access_key_id: Option<String>,
58 secret_access_key: Option<String>,
59 prefix: Option<String>,
60 allow_http: bool,
61}
62
63impl Default for S3StorageSettings {
64 fn default() -> Self {
65 Self {
66 endpoint: None,
67 region: "us-east-1".to_string(),
68 bucket: String::new(),
69 access_key_id: None,
70 secret_access_key: None,
71 prefix: None,
72 allow_http: false,
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn storage_settings_defaults() {
83 let settings = StorageSettings::default();
84 assert_eq!(*settings.get_backend(), BackendType::Local);
85 assert_eq!(
86 settings.get_local().get_root_dir(),
87 &PathBuf::from("./storage")
88 );
89 assert_eq!(settings.get_s3().get_region(), "us-east-1");
90 assert_eq!(settings.get_s3().get_bucket(), "");
91 assert!(!settings.get_s3().get_allow_http());
92 assert!(settings.get_s3().get_endpoint().is_none());
93 assert!(settings.get_s3().get_access_key_id().is_none());
94 assert!(settings.get_s3().get_secret_access_key().is_none());
95 assert!(settings.get_s3().get_prefix().is_none());
96 }
97
98 #[test]
99 fn storage_settings_deserialize() {
100 let json = r#"{"backend":"s3","s3":{"bucket":"my-bucket","region":"cn-north-1","endpoint":"http://127.0.0.1:9000","allow_http":true}}"#;
101 let settings: StorageSettings = serde_json::from_str(json).unwrap();
102 assert_eq!(*settings.get_backend(), BackendType::S3);
103 assert_eq!(settings.get_s3().get_bucket(), "my-bucket");
104 assert_eq!(settings.get_s3().get_region(), "cn-north-1");
105 assert!(settings.get_s3().get_allow_http());
106 assert_eq!(
107 settings.get_local().get_root_dir(),
108 &PathBuf::from("./storage")
109 );
110 }
111}
No newline at end of file
Added lib/config/src/user.rs lib/config/src/user.rs +158 −0 View lib/config/src/user.rs
@@ -0,0 +1,158 @@
1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum ProfileVisibility {
9 Public,
10 #[default]
11 RegisteredOnly,
12 MembersOnly,
13 Private,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct UserConfig {
18 #[serde(default = "default_max_pagination")]
19 pub max_profile_pagination: usize,
20 #[serde(default = "default_default_pagination")]
21 pub default_profile_pagination: usize,
22 #[serde(default = "default_max_username_len")]
23 pub max_username_len: usize,
24 #[serde(default = "default_min_username_len")]
25 pub min_username_len: usize,
26 #[serde(default = "default_max_display_name_len")]
27 pub max_display_name_len: usize,
28 #[serde(default = "default_max_bio_len")]
29 pub max_bio_len: usize,
30 #[serde(default = "default_max_pronouns_len")]
31 pub max_pronouns_len: usize,
32 #[serde(default = "default_max_website_len")]
33 pub max_website_len: usize,
34 #[serde(default = "default_max_location_len")]
35 pub max_location_len: usize,
36 #[serde(default = "default_max_company_len")]
37 pub max_company_len: usize,
38 #[serde(default = "default_max_meme_name_len")]
39 pub max_meme_name_len: usize,
40 #[serde(default = "default_max_recovery_codes")]
41 pub max_recovery_codes: usize,
42 #[serde(default = "default_access_key_token_bytes")]
43 pub access_key_token_bytes: usize,
44 #[serde(default = "default_ssh_key_max_per_user")]
45 pub ssh_key_max_per_user: usize,
46 #[serde(default = "default_deletion_grace_days")]
47 pub deletion_grace_days: i64,
48 #[serde(default = "default_follow_rate_limit_secs")]
49 pub follow_rate_limit_secs: u64,
50 #[serde(default = "default_block_rate_limit_secs")]
51 pub block_rate_limit_secs: u64,
52 #[serde(default = "default_report_rate_limit_secs")]
53 pub report_rate_limit_secs: u64,
54 #[serde(default = "default_unread_cache_ttl")]
55 pub notifications_unread_cache_ttl_secs: u64,
56 #[serde(default)]
57 pub default_visibility: ProfileVisibility,
58 #[serde(default = "default_default_locale")]
59 pub default_locale: String,
60 #[serde(default = "default_default_timezone")]
61 pub default_timezone: String,
62}
63
64fn default_max_pagination() -> usize {
65 100
66}
67fn default_default_pagination() -> usize {
68 20
69}
70fn default_max_username_len() -> usize {
71 64
72}
73fn default_min_username_len() -> usize {
74 1
75}
76fn default_max_display_name_len() -> usize {
77 64
78}
79fn default_max_bio_len() -> usize {
80 512
81}
82fn default_max_pronouns_len() -> usize {
83 32
84}
85fn default_max_website_len() -> usize {
86 256
87}
88fn default_max_location_len() -> usize {
89 128
90}
91fn default_max_company_len() -> usize {
92 128
93}
94fn default_max_meme_name_len() -> usize {
95 64
96}
97fn default_max_recovery_codes() -> usize {
98 10
99}
100fn default_access_key_token_bytes() -> usize {
101 32
102}
103fn default_ssh_key_max_per_user() -> usize {
104 50
105}
106fn default_deletion_grace_days() -> i64 {
107 14
108}
109fn default_follow_rate_limit_secs() -> u64 {
110 60
111}
112fn default_block_rate_limit_secs() -> u64 {
113 60
114}
115fn default_report_rate_limit_secs() -> u64 {
116 300
117}
118fn default_unread_cache_ttl() -> u64 {
119 3600
120}
121fn default_default_locale() -> String {
122 "en".to_string()
123}
124fn default_default_timezone() -> String {
125 "UTC".to_string()
126}
127
128impl Default for UserConfig {
129 fn default() -> Self {
130 Self {
131 max_profile_pagination: default_max_pagination(),
132 default_profile_pagination: default_default_pagination(),
133 max_username_len: default_max_username_len(),
134 min_username_len: default_min_username_len(),
135 max_display_name_len: default_max_display_name_len(),
136 max_bio_len: default_max_bio_len(),
137 max_pronouns_len: default_max_pronouns_len(),
138 max_website_len: default_max_website_len(),
139 max_location_len: default_max_location_len(),
140 max_company_len: default_max_company_len(),
141 max_meme_name_len: default_max_meme_name_len(),
142 max_recovery_codes: default_max_recovery_codes(),
143 access_key_token_bytes: default_access_key_token_bytes(),
144 ssh_key_max_per_user: default_ssh_key_max_per_user(),
145 deletion_grace_days: default_deletion_grace_days(),
146 follow_rate_limit_secs: default_follow_rate_limit_secs(),
147 block_rate_limit_secs: default_block_rate_limit_secs(),
148 report_rate_limit_secs: default_report_rate_limit_secs(),
149 notifications_unread_cache_ttl_secs: default_unread_cache_ttl(),
150 default_visibility: ProfileVisibility::default(),
151 default_locale: default_default_locale(),
152 default_timezone: default_default_timezone(),
153 }
154 }
155}
156
157#[allow(dead_code)]
158pub type SharedUserConfig = Arc<UserConfig>;
No newline at end of file
Added lib/config/Cargo.toml lib/config/Cargo.toml +23 −0 View lib/config/Cargo.toml
@@ -0,0 +1,23 @@
1[package]
2name = "config"
3version.workspace = true
4edition.workspace = true
5authors.workspace = true
6categories.workspace = true
7keywords.workspace = true
8repository.workspace = true
9homepage.workspace = true
10
11[lib]
12path = "src/lib.rs"
13name = "config"
14
15[dependencies]
16serde = { workspace = true }
17serde_json = { workspace = true }
18time = { workspace = true }
19lombok = { workspace = true }
20utoipa = { workspace = true }
21thiserror = { workspace = true }
22config-rs = { workspace = true }
23dotenvy = "0.15"
No newline at end of file