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