1use std::path::PathBuf;
4
5use crate::{ColumnKind, Store, Value};
6
7pub 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
63pub struct Settings {
65 pub host: String,
67 pub port: u16,
69 pub debug: bool,
71 pub forgery: bool,
73 pub secret: Option<String>,
75 pub assets: PathBuf,
77}
78
79impl Settings {
80 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 pub fn host(mut self, host: &str) -> Self {
95 self.host = host.to_string();
96 self
97 }
98
99 pub fn port(mut self, port: u16) -> Self {
101 self.port = port;
102 self
103 }
104
105 pub fn forgery(mut self, on: bool) -> Self {
107 self.forgery = on;
108 self
109 }
110
111 pub fn secret(mut self, secret: impl Into<String>) -> Self {
113 self.secret = Some(secret.into());
114 self
115 }
116
117 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}