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
60
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
60
        match self {
76
60
            ConfigOption::String(s) => write!(f, "{s}"),
77
            ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
78
        }
79
60
    }
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
120
    fn from(value: &str) -> Self {
146
120
        ConfigOption::String(value.to_string())
147
120
    }
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
// The config read/write logic is identical for the system (admin DB) and
169
// per-user surfaces — only the connection differs. The `*_on` helpers take an
170
// explicit connection so both surfaces share one implementation; `User::config`
171
// / `User::set_config` (see `user.rs`) pass a per-user connection, while the
172
// `system_*` wrappers below pass the global admin connection.
173

            
174
/// Fetches the `contents` of a field from the configdata.
175
///
176
/// This function attempts to retrieve the configuration stored in the
177
/// `configdata` table.  If the field is not found, it returns a
178
/// `ConfigError::NoConfig`.
179
///
180
/// # Arguments
181
///
182
/// - `field`: The name of the field to retrieve.
183
///
184
/// # Returns
185
///
186
/// - `Ok(Some(ConfigOption))` if the field exists.
187
/// - `Err(ConfigError::NoConfig)` if the field is absent from both the
188
///   `config` and `configdata` tables (callers that treat "absent" as
189
///   "not set" — e.g. the `GetConfig` command — normalise this to `Ok(None)`).
190
/// - `Err(ConfigError)` on any other retrieval error.
191
///
192
/// # Example
193
///
194
/// ```rust,ignore
195
/// let config = system_configdata("example_field").await?;
196
/// if let Some(option) = config {
197
///     println!("Config value: {}", option);
198
/// }
199
/// ```
200
///
201
/// # Errors
202
///
203
/// Returns a `ConfigError::DB` if there is an issue with the database connection.
204
151
async fn configdata_on(
205
151
    conn: &mut sqlx::PgConnection,
206
151
    field: &str,
207
151
) -> Result<Option<ConfigOption>, ConfigError> {
208
151
    let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
209
151
        .fetch_one(&mut *conn)
210
151
        .await
211
151
        .map_err(|err| {
212
150
            if let RowNotFound = err {
213
150
                ConfigError::NoConfig(String::from(field))
214
            } else {
215
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
216
                ConfigError::DB
217
            }
218
150
        })?;
219

            
220
1
    Ok(Some(ConfigOption::Blob(value)))
221
151
}
222

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

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

            
277
153
    Ok(())
278
153
}
279

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

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

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

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

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

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

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

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

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

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