1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
use askama::Template;
use axum::{
Form, Router,
extract::State,
response::{Html, Redirect},
routing::{get, post},
};
use serde::Deserialize;
use crate::auth::CurrentUser;
use crate::routes::AppState;
#[derive(Template)]
#[template(path = "players/manage.html")]
struct PlayerManagementTemplate {
username: String,
is_superuser: bool,
password_change_required: bool,
active_instance_name: Option<String>,
whitelist: Vec<String>,
ops: Vec<String>,
bans: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct PlayerAction {
pub username: String,
}
pub fn router() -> Router<AppState> {
Router::new()
.route("/players", get(manage_players))
.route("/players/whitelist/add", post(add_whitelist))
.route("/players/whitelist/remove", post(remove_whitelist))
.route("/players/op/add", post(add_op))
.route("/players/op/remove", post(remove_op))
.route("/players/ban/add", post(add_ban))
.route("/players/ban/remove", post(remove_ban))
}
async fn manage_players(
State(state): State<AppState>,
CurrentUser(account): CurrentUser,
) -> Result<Html<String>, axum::response::Redirect> {
let active = get_active_instance(&state.db);
let (instance_name, whitelist, ops, bans) =
if let Some((_id, inst_name, _rcon_pw, _container_state)) = active {
let data_dir = state
.config
.server_directory
.join("instances")
.join(&inst_name)
.join("data");
let w = read_json_list(&data_dir.join("whitelist.json"));
let o = read_json_list(&data_dir.join("ops.json"));
let b = read_json_list(&data_dir.join("banned-players.json"));
(Some(inst_name), w, o, b)
} else {
(None, vec![], vec![], vec![])
};
let template = PlayerManagementTemplate {
username: account.username,
is_superuser: account.is_superuser,
password_change_required: account.password_change_required,
active_instance_name: instance_name,
whitelist,
ops,
bans,
};
Ok(Html(template.render().unwrap_or_default()))
}
fn read_json_list(path: &std::path::Path) -> Vec<String> {
if let Ok(contents) = std::fs::read_to_string(path)
&& let Ok(entries) = serde_json::from_str::<Vec<serde_json::Value>>(&contents) {
return entries
.iter()
.filter_map(|e| e["name"].as_str().map(String::from))
.collect();
}
vec![]
}
macro_rules! player_action {
($name:ident, $command:expr) => {
async fn $name(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
Form(form): Form<PlayerAction>,
) -> Result<Redirect, axum::response::Redirect> {
if let Some((_, _, rcon_pw, _)) = get_active_instance(&state.db) {
let client = crate::rcon::RconClient::new(rcon_pw);
let cmd = format!($command, form.username);
match client.send_command_async(cmd.clone()).await {
Ok(resp) => tracing::info!("RCON '{}' -> {}", cmd, resp.trim()),
Err(e) => tracing::error!("RCON '{}' failed: {}", cmd, e),
}
} else {
tracing::warn!(
"Player action '{}' called but no active instance",
stringify!($name)
);
}
Ok(Redirect::to("/players"))
}
};
}
player_action!(add_whitelist, "whitelist add {}");
player_action!(remove_whitelist, "whitelist remove {}");
player_action!(add_op, "op {}");
player_action!(remove_op, "deop {}");
player_action!(add_ban, "ban {}");
player_action!(remove_ban, "pardon {}");
fn get_active_instance(db: &crate::db::Database) -> Option<(i64, String, String, String)> {
db.conn()
.query_row(
"SELECT id, name, rcon_password, container_state FROM instances WHERE is_active = 1",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.ok()
}
|