Skip to main content

rango_core/
settings.rs

1//! Runtime settings and the secret key.
2
3use std::path::PathBuf;
4
5use crate::{ColumnKind, Store, Value};
6
7/// Persistent secret key for the store, generated on first use.
8pub async fn key(store: &dyn Store) -> String {
9    if store
10        .execute(
11            "CREATE TABLE IF NOT EXISTS setting (name TEXT PRIMARY KEY, value TEXT)",
12            &[],
13        )
14        .await
15        .is_err()
16    {
17        return ephemeral("no setting table");
18    }
19    if let Some(secret) = read(store).await {
20        return secret;
21    }
22    let secret = fresh();
23    let _ = store
24        .execute(
25            "INSERT OR IGNORE INTO setting (name, value) VALUES ('secret', ?)",
26            &[Value::str(secret.clone())],
27        )
28        .await;
29    match read(store).await {
30        Some(secret) => secret,
31        None => ephemeral("no secret row"),
32    }
33}
34
35async fn read(store: &dyn Store) -> Option<String> {
36    let rows = store
37        .fetch(
38            "SELECT value FROM setting WHERE name = 'secret'",
39            &[],
40            &[ColumnKind::Text],
41        )
42        .await
43        .ok()?;
44    match rows.first()?.get(0)? {
45        Value::Str(secret) if !secret.is_empty() => Some(secret.clone()),
46        _ => None,
47    }
48}
49
50fn fresh() -> String {
51    format!(
52        "{}{}",
53        uuid::Uuid::new_v4().simple(),
54        uuid::Uuid::new_v4().simple()
55    )
56}
57
58fn ephemeral(why: &str) -> String {
59    tracing::warn!("ephemeral secret: {why}");
60    fresh()
61}
62
63/// Runtime settings for a Rango app.
64pub struct Settings {
65    /// Host address to bind.
66    pub host: String,
67    /// Port to listen on.
68    pub port: u16,
69    /// Show debug panic messages in responses.
70    pub debug: bool,
71    /// Enable the CSRF guard middleware.
72    pub forgery: bool,
73    /// Secret used for per-install signing; required before serving.
74    pub secret: Option<String>,
75    /// Directory served as static assets.
76    pub assets: PathBuf,
77}
78
79impl Settings {
80    /// Default settings for local development.
81    pub fn new() -> Self {
82        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
83        Self {
84            host: "127.0.0.1".to_string(),
85            port: 8000,
86            debug: true,
87            forgery: true,
88            secret: None,
89            assets: cwd.join("assets"),
90        }
91    }
92
93    /// Set the host to bind.
94    pub fn host(mut self, host: &str) -> Self {
95        self.host = host.to_string();
96        self
97    }
98
99    /// Set the port to listen on.
100    pub fn port(mut self, port: u16) -> Self {
101        self.port = port;
102        self
103    }
104
105    /// Enable or disable the CSRF guard.
106    pub fn forgery(mut self, on: bool) -> Self {
107        self.forgery = on;
108        self
109    }
110
111    /// Set the install secret.
112    pub fn secret(mut self, secret: impl Into<String>) -> Self {
113        self.secret = Some(secret.into());
114        self
115    }
116
117    /// Set the base directory; assets resolve to its `assets` subdir.
118    pub fn base_dir(mut self, dir: impl Into<PathBuf>) -> Self {
119        self.assets = dir.into().join("assets");
120        self
121    }
122}
123
124impl Default for Settings {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130#[cfg(all(test, feature = "sqlite"))]
131mod tests {
132    use super::*;
133    use crate::store::sqlite;
134
135    #[tokio::test]
136    async fn keeps() {
137        let path = std::env::temp_dir().join(format!("rango-secret-{}.sqlite", std::process::id()));
138        let _ = std::fs::remove_file(&path);
139        let store = sqlite::open(&path).await.unwrap();
140        let first = key(store.as_ref()).await;
141        assert_eq!(first.len(), 64);
142        assert_eq!(key(store.as_ref()).await, first);
143        let _ = std::fs::remove_file(&path);
144    }
145}