Skip to main content

rush_sync_server/server/
settings.rs

1use base64::Engine;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ServerSettings {
7    #[serde(default)]
8    pub custom_404_enabled: bool,
9    #[serde(default = "default_404_path")]
10    pub custom_404_path: String,
11    #[serde(default)]
12    pub pin_enabled: bool,
13    #[serde(default)]
14    pub pin_code: String,
15}
16
17fn default_404_path() -> String {
18    "404.html".to_string()
19}
20
21impl Default for ServerSettings {
22    fn default() -> Self {
23        Self {
24            custom_404_enabled: false,
25            custom_404_path: default_404_path(),
26            pin_enabled: false,
27            pin_code: String::new(),
28        }
29    }
30}
31
32impl ServerSettings {
33    /// Get the settings file path for a server directory
34    pub fn settings_path(server_dir: &Path) -> PathBuf {
35        server_dir.join(".rss-settings.json")
36    }
37
38    /// Load settings from the server directory
39    pub fn load(server_dir: &Path) -> Self {
40        let path = Self::settings_path(server_dir);
41        if path.exists() {
42            match std::fs::read_to_string(&path) {
43                Ok(content) => {
44                    serde_json::from_str(&content).unwrap_or_default()
45                }
46                Err(e) => {
47                    log::warn!("Failed to read settings: {}", e);
48                    Self::default()
49                }
50            }
51        } else {
52            Self::default()
53        }
54    }
55
56    /// Save settings to the server directory
57    pub fn save(&self, server_dir: &Path) -> Result<(), std::io::Error> {
58        let path = Self::settings_path(server_dir);
59        let content = serde_json::to_string_pretty(self)
60            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
61        std::fs::write(&path, content)
62    }
63
64    /// Get the server directory path from server name and port
65    pub fn get_server_dir(server_name: &str, port: u16) -> Option<PathBuf> {
66        let base_dir = crate::core::helpers::get_base_dir().ok()?;
67        Some(
68            base_dir
69                .join("www")
70                .join(format!("{}-[{}]", server_name, port)),
71        )
72    }
73
74    /// Encode a PIN for storage (Base64 obfuscation)
75    pub fn encode_pin(plain: &str) -> String {
76        base64::engine::general_purpose::STANDARD.encode(plain.as_bytes())
77    }
78
79    /// Decode a stored PIN
80    fn decode_pin(encoded: &str) -> Option<String> {
81        base64::engine::general_purpose::STANDARD
82            .decode(encoded)
83            .ok()
84            .and_then(|bytes| String::from_utf8(bytes).ok())
85    }
86
87    /// Verify a plain PIN against the stored encoded PIN
88    pub fn verify_pin(&self, input: &str) -> bool {
89        if !self.pin_enabled || self.pin_code.is_empty() {
90            return true;
91        }
92        // Try decoding (new format) or direct compare (legacy)
93        match Self::decode_pin(&self.pin_code) {
94            Some(decoded) => decoded == input,
95            None => self.pin_code == input,
96        }
97    }
98
99    /// Auto-create the 404.html file if it doesn't exist
100    pub fn ensure_404_page(&self, server_dir: &Path, server_name: &str) {
101        if !self.custom_404_enabled {
102            return;
103        }
104        let page_path = server_dir.join(&self.custom_404_path);
105        if page_path.exists() {
106            return;
107        }
108        let html = format!(
109            r#"<!DOCTYPE html>
110<html lang="en">
111<head>
112<meta charset="UTF-8">
113<meta name="viewport" content="width=device-width, initial-scale=1.0">
114<title>404 — {name}</title>
115<link rel="icon" href="/.rss/favicon.svg" type="image/svg+xml">
116<style>
117:root {{
118  --bg: #0a0a0f;
119  --card: #12121a;
120  --border: #2a2a3a;
121  --text: #e4e4ef;
122  --dim: #8888a0;
123  --muted: #55556a;
124  --accent: #6c63ff;
125}}
126*{{ margin:0; padding:0; box-sizing:border-box; }}
127body {{
128  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
129  background: var(--bg);
130  color: var(--text);
131  min-height: 100vh;
132  display: flex;
133  align-items: center;
134  justify-content: center;
135  overflow: hidden;
136}}
137.grain {{
138  position:fixed; top:0; left:0; width:100%; height:100%;
139  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
140  pointer-events:none; z-index:1000;
141}}
142.orb {{
143  position:fixed; border-radius:50%; filter:blur(90px); opacity:0.08; pointer-events:none;
144}}
145.orb.p {{ background:#6c63ff; width:350px; height:350px; top:-80px; right:-50px; }}
146.orb.c {{ background:#22d3ee; width:250px; height:250px; bottom:100px; left:-60px; }}
147.box {{
148  background: var(--card);
149  border: 1px solid var(--border);
150  border-radius: 12px;
151  padding: 48px;
152  text-align: center;
153  max-width: 440px;
154  position: relative;
155  z-index: 1;
156}}
157.code {{
158  font-size: 72px;
159  font-weight: 800;
160  color: var(--accent);
161  letter-spacing: -2px;
162  line-height: 1;
163  margin-bottom: 12px;
164}}
165.msg {{
166  font-size: 16px;
167  color: var(--dim);
168  margin-bottom: 28px;
169  line-height: 1.5;
170}}
171.server {{
172  font-family: 'SF Mono', 'Fira Code', monospace;
173  font-size: 11px;
174  font-weight: 700;
175  color: #fff;
176  background: var(--accent);
177  padding: 3px 12px;
178  border-radius: 100px;
179  display: inline-block;
180  margin-bottom: 24px;
181}}
182.links {{
183  display: flex;
184  gap: 8px;
185  justify-content: center;
186  flex-wrap: wrap;
187}}
188.links a {{
189  display: inline-block;
190  padding: 8px 20px;
191  border: 1px solid var(--border);
192  border-radius: 100px;
193  color: var(--dim);
194  text-decoration: none;
195  font-size: 12px;
196  font-weight: 600;
197  text-transform: uppercase;
198  letter-spacing: 0.05em;
199  transition: all 0.15s;
200}}
201.links a:hover {{
202  border-color: var(--accent);
203  color: var(--accent);
204}}
205.hint {{
206  font-size: 11px;
207  color: var(--muted);
208  margin-top: 20px;
209}}
210</style>
211</head>
212<body>
213<div class="grain"></div>
214<div class="orb p"></div>
215<div class="orb c"></div>
216<div class="box">
217  <span class="server">{name}</span>
218  <div class="code">404</div>
219  <div class="msg">This page doesn't exist yet.</div>
220  <div class="links">
221    <a href="/">Home</a>
222    <a href="/.rss/">Dashboard</a>
223  </div>
224  <div class="hint">Edit this file: {path}</div>
225</div>
226</body>
227</html>"#,
228            name = server_name,
229            path = self.custom_404_path
230        );
231        if let Err(e) = std::fs::write(&page_path, html) {
232            log::error!("Failed to create 404 page: {}", e);
233        } else {
234            log::info!("Created custom 404 page: {:?}", page_path);
235        }
236    }
237}