1pub mod db;
5pub mod help;
6pub mod server;
7
8use crate::db::DB;
9use colored::Colorize;
10use std::io::{self, Write};
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13use tokio::sync::watch;
14
15const SESSION_TIMEOUT_MINS: u64 = 20;
16
17pub struct AdminSession {
18 pub logged_in: bool,
19 pub last_active: Instant,
20 pub username: Option<String>,
21 pub is_admin: bool,
22}
23
24struct Command {
25 parts: Vec<String>,
26 #[allow(dead_code)]
27 params: Vec<String>,
28 raw: String,
29}
30
31impl Default for AdminSession {
32 fn default() -> Self {
33 Self {
34 logged_in: false,
35 is_admin: false,
36 last_active: Instant::now(),
37 username: None,
38 }
39 }
40}
41
42impl AdminSession {
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn is_valid(&self) -> bool {
49 self.logged_in
50 && self.last_active.elapsed() < Duration::from_secs(SESSION_TIMEOUT_MINS * 60)
51 }
52
53 pub fn update_is_admin(&mut self, is_admin: bool) {
54 self.is_admin = is_admin;
55 }
56
57 pub fn login(&mut self, username: String, is_admin: bool) {
58 self.logged_in = true;
59 self.username = Some(username);
60 self.is_admin = is_admin;
61 self.last_active = Instant::now();
62 }
63
64 pub fn logout(&mut self) {
65 self.logged_in = false;
66 self.username = None;
67 }
68}
69
70fn parse(input: &str) -> Option<Command> {
81 let trimmed = input.trim();
82 if trimmed.is_empty() {
83 return None;
84 }
85
86 let (cmd_part, params_part) = match trimmed.split_once(' ') {
87 Some((cmd, params)) => (cmd, params.trim()),
88 None => (trimmed, ""),
89 };
90
91 let parts: Vec<String> = cmd_part
92 .split(':')
93 .map(|s| s.trim().to_lowercase())
94 .filter(|s| !s.is_empty())
95 .collect();
96
97 let params: Vec<String> = if params_part.is_empty() {
98 vec![]
99 } else {
100 params_part
101 .split(", ")
102 .map(|s| s.trim().to_string())
103 .filter(|s| !s.is_empty())
104 .collect()
105 };
106
107 Some(Command {
108 parts,
109 params,
110 raw: trimmed.to_string(),
111 })
112}
113
114pub async fn spawn_cli(db: DB, server_start: Instant, shutdown_tx: watch::Sender<bool>) {
131 let session = Arc::new(Mutex::new(AdminSession::new()));
132 let handle = tokio::runtime::Handle::current();
133
134 tokio::task::spawn_blocking(move || {
135 std::thread::sleep(Duration::from_millis(600));
136
137 loop {
138 print!("{} ", ">".cyan().bold());
139 io::stdout().flush().unwrap();
140
141 let mut input = String::new();
142 match io::stdin().read_line(&mut input) {
143 Ok(0) => break, Ok(_) => {}
145 Err(_) => break,
146 }
147
148 {
149 let mut sess = session.lock().unwrap();
150 if sess.logged_in && !sess.is_valid() {
151 println!(
152 "{}",
153 "Session expired after 20 minutes of inactivity. Please run server:login again."
154 .yellow()
155 );
156 sess.logout();
157 }
158 }
159
160 let cmd = match parse(&input) {
161 Some(c) => c,
162 None => continue,
163 };
164
165 handle.block_on(dispatch(&cmd, &db, server_start, &session, &shutdown_tx));
167 }
168 });
169}
170
171async fn dispatch(
180 cmd: &Command,
181 db: &DB,
182 server_start: Instant,
183 session: &Arc<Mutex<AdminSession>>,
184 shutdown_tx: &watch::Sender<bool>,
185) {
186 match cmd.parts.as_slice() {
187 [ns] if ns == "help" => {
189 help::print_all();
190 }
191 [ns, command] if ns == "help" => {
192 help::print_command(command);
193 }
194
195 [ns, command] if ns == "server" => match command.as_str() {
197 "status" => server::status(server_start).await,
198 "logout" => server::logout(session),
199 "shutdown" => {
200 if !require_admin(session) {
201 return;
202 }
203 server::shutdown(db, server_start, shutdown_tx).await;
204 }
205 "logs" => {
206 let params = cmd.raw.split_once(' ').map(|x| x.1).unwrap_or("");
207 server::logs(db, params).await;
208 }
209 "audit" => {
210 let params = cmd.raw.split_once(' ').map(|x| x.1).unwrap_or("");
211 server::audit(db, params).await;
212 }
213 _ => unknown(&cmd.raw),
214 },
215
216 [ns, command] if ns == "db" => match command.as_str() {
218 "stats" => db::stats(db).await,
219 "table" => {
220 let params = cmd.raw.split_once(' ').map(|x| x.1).unwrap_or("");
221 db::table(db, params).await;
222 }
223 _ => unknown(&cmd.raw),
224 },
225
226 _ => unknown(&cmd.raw),
227 }
228}
229
230pub fn require_admin(session: &Arc<Mutex<AdminSession>>) -> bool {
252 let sess = session.lock().unwrap();
253 if !sess.is_valid() {
254 println!(
255 "{}",
256 "This command requires authentication. Please run server:login first.".red()
257 );
258 return false;
259 }
260 if !sess.is_admin {
261 println!("{}", "This command requires admin privileges.".red());
262 return false;
263 }
264 true
265}
266
267fn unknown(raw: &str) {
272 println!(
273 "{} {} {}",
274 "Unknown command:".red(),
275 raw.white(),
276 "- type `help` to see available commands.".dimmed()
277 );
278}