1
use crate::db::get_connection;
2
use derive_more::From;
3
use rust_i18n::t;
4
use sqlx::Error::RowNotFound;
5
use std::env::var;
6
use std::fmt;
7
use std::ops::Deref;
8
use std::sync::LazyLock;
9
use thiserror::Error;
10

            
11
/// Represents an error that can occur while accessing the configuration.
12
///
13
/// # Variants
14
///
15
/// - `NoConfig(String)`: Returned when a specific configuration field is not found.
16
/// - `DB`: Indicates a database access error.
17
/// - `Sqlx`: An error propagated from the `SQLx` crate.
18
///
19
/// # Example
20
///
21
/// ```rust
22
/// use thiserror::Error;
23
///
24
/// #[derive(Debug, Error)]
25
/// pub enum ConfigError {
26
///     #[error("No such config field: {0}")]
27
///     NoConfig(String),
28
///     #[error("Can't access db")]
29
///     DB,
30
///     #[error("Sqlx")]
31
///     Sqlx(#[from] sqlx::Error),
32
/// }
33
/// ```
34
#[derive(Debug, Error)]
35
pub enum ConfigError {
36
    #[error("No such config field: {0}")]
37
    NoConfig(String),
38
    #[error("Can't access db")]
39
    DB,
40
    #[error("Sqlx")]
41
    Sqlx(#[from] sqlx::Error),
42
}
43

            
44
/// Represents a configuration value stored in the system.
45
///
46
/// # Variants
47
///
48
/// - `String`: Stores the configuration as a string.
49
/// - `Blob`: Stores the configuration as a binary blob.
50
///
51
/// # Example
52
///
53
/// ```rust
54
/// use server::config::ConfigOption;
55
/// let config_str = ConfigOption::String("example".to_string());
56
/// let config_blob = ConfigOption::Blob(vec![1, 2, 3, 4]);
57
/// ```
58
///
59
/// You can also convert `String` and `Vec<u8>` directly into `ConfigOption`
60
/// using the `From` trait:
61
///
62
/// ```rust
63
/// use server::config::ConfigOption;
64
/// let config: ConfigOption = "example".to_string().into();
65
/// let blob: ConfigOption = vec![1, 2, 3, 4].into();
66
/// ```
67
#[derive(Debug, From, PartialEq, Clone)]
68
pub enum ConfigOption {
69
    String(#[from] String),
70
    Blob(#[from] Vec<u8>),
71
}
72

            
73
impl fmt::Display for ConfigOption {
74
54
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
54
        match self {
76
54
            ConfigOption::String(s) => write!(f, "{s}"),
77
            ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
78
        }
79
54
    }
80
}
81

            
82
/// Convert `ConfigOption` into a `&str` reference when the variant is a `String`.
83
///
84
/// # Panics
85
///
86
/// This will panic if the `ConfigOption` is of type `Blob`, as it cannot be
87
/// converted to a `&str`.
88
///
89
/// # Example
90
///
91
/// ```rust,should_panic
92
/// use server::config::ConfigOption;
93
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
94
/// let str_ref: &str = config.as_ref(); // Will panic
95
/// ```
96
impl AsRef<str> for ConfigOption {
97
    fn as_ref(&self) -> &str {
98
        match self {
99
            ConfigOption::String(s) => s.as_str(),
100
            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be converted to &str"),
101
        }
102
    }
103
}
104

            
105
/// Dereference `ConfigOption::String` to `&str`.
106
///
107
/// This allows you to use `ConfigOption` directly in contexts where a `&str`
108
/// is expected, without having to manually call `.as_ref()`.
109
///
110
/// # Panics
111
///
112
/// This will panic if the `ConfigOption` is of type `Blob`.
113
///
114
/// # Example
115
///
116
/// ```rust,should_panic
117
/// use server::config::ConfigOption;
118
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
119
/// let str_ref: &str = &*config; // Will panic
120
/// ```
121
impl Deref for ConfigOption {
122
    type Target = str;
123

            
124
    fn deref(&self) -> &Self::Target {
125
        match self {
126
            ConfigOption::String(s) => s.as_str(),
127
            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be dereferenced as &str"),
128
        }
129
    }
130
}
131

            
132
/// Allows for the conversion from a `&str` to `ConfigOption`.
133
///
134
/// This is useful for passing string literals or string slices directly as `ConfigOption`.
135
/// The conversion will wrap the `&str` in the `ConfigOption::String` variant.
136
///
137
/// # Example
138
///
139
/// ```rust
140
/// use server::config::ConfigOption;
141
/// let config_option: ConfigOption = "example text".into();
142
/// assert_eq!(config_option, ConfigOption::String("example text".to_string()));
143
/// ```
144
impl From<&str> for ConfigOption {
145
108
    fn from(value: &str) -> Self {
146
108
        ConfigOption::String(value.to_string())
147
108
    }
148
}
149

            
150
/// Convert `ConfigOption` into a `&[u8]` reference when the variant is a `Blob`.
151
///
152
/// # Example
153
///
154
/// ```rust
155
/// use server::config::ConfigOption;
156
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
157
/// let byte_ref: &[u8] = config.as_ref(); // Works
158
/// ```
159
impl AsRef<[u8]> for ConfigOption {
160
    fn as_ref(&self) -> &[u8] {
161
        match self {
162
            ConfigOption::String(s) => s.as_bytes(),
163
            ConfigOption::Blob(b) => b.as_slice(),
164
        }
165
    }
166
}
167

            
168
/// Fetches the `contents` of a field from the configdata.
169
///
170
/// This function attempts to retrieve the configuration stored in the
171
/// `configdata` table.  If the field is not found, it returns a
172
/// `ConfigError::NoConfig`.
173
///
174
/// # Arguments
175
///
176
/// - `field`: The name of the field to retrieve.
177
///
178
/// # Returns
179
///
180
/// - `Ok(Some(ConfigOption))` if the field exists.
181
/// - `Ok(None)` if the field doesn't exist.
182
/// - `Err(ConfigError)` if there is an error during retrieval.
183
///
184
/// # Example
185
///
186
/// ```rust,ignore
187
/// let config = system_configdata("example_field").await?;
188
/// if let Some(option) = config {
189
///     println!("Config value: {}", option);
190
/// }
191
/// ```
192
///
193
/// # Errors
194
///
195
/// Returns a `ConfigError::DB` if there is an issue with the database connection.
196
// The config read/write logic is identical for the system (admin DB) and
197
// per-user surfaces — only the connection differs. The `*_on` helpers take an
198
// explicit connection so both surfaces share one implementation; `User::config`
199
// / `User::set_config` (see `user.rs`) pass a per-user connection, while the
200
// `system_*` wrappers below pass the global admin connection.
201

            
202
136
async fn configdata_on(
203
136
    conn: &mut sqlx::PgConnection,
204
136
    field: &str,
205
136
) -> Result<Option<ConfigOption>, ConfigError> {
206
136
    let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
207
136
        .fetch_one(&mut *conn)
208
136
        .await
209
136
        .map_err(|err| {
210
135
            if let RowNotFound = err {
211
135
                ConfigError::NoConfig(String::from(field))
212
            } else {
213
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
214
                ConfigError::DB
215
            }
216
135
        })?;
217

            
218
1
    Ok(Some(ConfigOption::Blob(value)))
219
136
}
220

            
221
/// Reads a config field on the given connection: the `config` (string) table
222
/// first, falling back to `configdata` (blob).
223
///
224
/// # Errors
225
/// `ConfigError::DB` on a database error.
226
246
pub async fn config_on(
227
246
    conn: &mut sqlx::PgConnection,
228
246
    field: &str,
229
246
) -> Result<Option<ConfigOption>, ConfigError> {
230
246
    let value = sqlx::query_file_scalar!("../server/sql/select/config/string.sql", field)
231
246
        .fetch_one(&mut *conn)
232
246
        .await;
233
136
    match value {
234
110
        Ok(l) => Ok(Some(ConfigOption::String(l))),
235
136
        Err(RowNotFound) => configdata_on(conn, field).await,
236
        Err(err) => {
237
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
238
            Err(ConfigError::DB)
239
        }
240
    }
241
246
}
242

            
243
/// Upserts a config field on the given connection (string → `config`, blob →
244
/// `configdata`). The upsert keys on `lower(field)` (see the SQL), so it is
245
/// idempotent and case-insensitive.
246
///
247
/// # Errors
248
/// `ConfigError::DB` on a database error, `ConfigError::NoConfig` if the row is
249
/// missing for an update.
250
138
pub async fn set_config_on(
251
138
    conn: &mut sqlx::PgConnection,
252
138
    field: &str,
253
138
    contents: ConfigOption,
254
138
) -> Result<(), ConfigError> {
255
138
    let id = uuid::Uuid::new_v4();
256
138
    match contents {
257
137
        ConfigOption::String(s) => {
258
137
            sqlx::query_file!("../server/sql/set/config/string.sql", &id, field, s)
259
        }
260
1
        ConfigOption::Blob(b) => {
261
1
            sqlx::query_file!("../server/sql/set/config/blob.sql", &id, field, b)
262
        }
263
    }
264
138
    .execute(&mut *conn)
265
138
    .await
266
138
    .map_err(|err| {
267
        if let RowNotFound = err {
268
            ConfigError::NoConfig(String::from(field))
269
        } else {
270
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
271
            ConfigError::DB
272
        }
273
    })?;
274

            
275
138
    Ok(())
276
138
}
277

            
278
/// Reads a SYSTEM (server-wide) config field from the global admin database.
279
/// For server-wide keys (infra URLs, locale) and the bootstrap reads that run
280
/// before any user is resolved. Per-user config goes through `User::config`.
281
///
282
/// # Errors
283
/// `ConfigError::DB` on a database error.
284
110
pub async fn system_config(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
285
110
    let mut conn = get_connection().await.map_err(|err| {
286
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
287
        ConfigError::DB
288
    })?;
289
110
    config_on(&mut conn, field).await
290
110
}
291

            
292
/// Reads a SYSTEM config blob field from the global admin database.
293
///
294
/// # Errors
295
/// `ConfigError::DB` on a database error.
296
pub async fn system_configdata(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
297
    let mut conn = get_connection().await.map_err(|err| {
298
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
299
        ConfigError::DB
300
    })?;
301
    configdata_on(&mut conn, field).await
302
}
303

            
304
/// Writes a SYSTEM config field to the global admin database.
305
///
306
/// # Errors
307
/// `ConfigError::DB` on a database error.
308
2
pub async fn set_system_config(field: &str, contents: ConfigOption) -> Result<(), ConfigError> {
309
2
    let mut conn = get_connection().await.map_err(|err| {
310
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
311
        ConfigError::DB
312
    })?;
313
2
    set_config_on(&mut conn, field, contents).await
314
2
}
315

            
316
static LOCALE_NAME: LazyLock<String> = LazyLock::new(|| var("LANG").unwrap_or(String::from("en")));
317

            
318
pub async fn load_config() -> Result<(), ConfigError> {
319
    if let Ok(Some(locale)) = system_config("locale").await {
320
        rust_i18n::set_locale(&locale);
321
        log::info!(
322
            "{}",
323
            t!(
324
                "Loaded the %{loc} locale from config, setting as main",
325
                loc = &locale
326
            )
327
        );
328
    } else {
329
        rust_i18n::set_locale(&LOCALE_NAME);
330
    }
331
    Ok(())
332
}
333

            
334
#[cfg(test)]
335
mod config_tests {
336
    use super::*;
337
    use crate::db::DB_POOL;
338
    use sqlx::PgPool;
339
    use supp_macro::local_db_sqlx_test;
340
    use tokio::sync::OnceCell;
341

            
342
    /// Context for keeping environment intact
343
    static CONTEXT: OnceCell<()> = OnceCell::const_new();
344

            
345
2
    async fn setup() {
346
2
        CONTEXT
347
2
            .get_or_init(|| async {
348
                #[cfg(feature = "testlog")]
349
1
                let _ = env_logger::builder()
350
1
                    .is_test(true)
351
1
                    .filter_level(log::LevelFilter::Trace)
352
1
                    .try_init();
353
2
            })
354
2
            .await;
355
2
    }
356

            
357
    #[local_db_sqlx_test]
358
    async fn test_config_string(pool: PgPool) {
359
        let opt = ConfigOption::String("testval".to_string());
360
        set_system_config("testfield", opt.clone()).await.unwrap();
361
        let val = system_config("testfield").await.unwrap().unwrap();
362
        assert_eq!(val, opt);
363
    }
364

            
365
    #[local_db_sqlx_test]
366
    async fn test_config_configdata(pool: PgPool) {
367
        let opt = ConfigOption::Blob(vec![0, 1, 2, 3]);
368
        set_system_config("testblob", opt.clone()).await.unwrap();
369
        let val = system_config("testblob").await.unwrap().unwrap();
370
        assert_eq!(val, opt);
371
    }
372
}