rush_sync_server/server/
settings.rs1use 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 pub fn settings_path(server_dir: &Path) -> PathBuf {
35 server_dir.join(".rss-settings.json")
36 }
37
38 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 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 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 pub fn encode_pin(plain: &str) -> String {
76 base64::engine::general_purpose::STANDARD.encode(plain.as_bytes())
77 }
78
79 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 pub fn verify_pin(&self, input: &str) -> bool {
89 if !self.pin_enabled || self.pin_code.is_empty() {
90 return true;
91 }
92 match Self::decode_pin(&self.pin_code) {
94 Some(decoded) => decoded == input,
95 None => self.pin_code == input,
96 }
97 }
98
99 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}