From f13ab1a6295efded498656aec10fb3bdaf187fc3 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 13:44:15 +0100 Subject: [PATCH] Service: set-password maintenance command for forgotten passwords Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ server/src/main.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index cacdf7c..54ea47f 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ A small Rust service (axum) in `server/` keeps the user accounts (Argon2 passwor | `GET /api/saves/{id}` | savegame JSON | | `DELETE /api/saves/{id}` | delete a savegame | +A forgotten password is set again on the server with `docker compose stop saves && docker compose run --rm --no-deps saves dungeoncraft-saves set-password '' && docker compose up -d saves` (ends the user's sessions). To start over with no accounts at all, delete `users.json` and `sessions.json` in the `savegames` volume. + All requests except health and login need `Authorization: Bearer `. Limits: 1 MB per savegame, 500 MB in total, 30 write requests and 20 login attempts per minute. Settings: `SAVE_DIR` (default `/data`), `LISTEN` (default `0.0.0.0:8080`). ## Adding a language diff --git a/server/src/main.rs b/server/src/main.rs index e213903..4f9b433 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -470,9 +470,33 @@ async fn delete_save(State(st): State, headers: HeaderMap, Path(id): P StatusCode::NO_CONTENT.into_response() } +// Maintenance: "dungeoncraft-saves set-password " sets a user's password +// (stop the service first or restart it afterwards; the user's sessions end). +fn set_password_cli(dir: &std::path::Path, name: &str, password: &str) { + let path = dir.join("users.json"); + let mut users: Vec = std::fs::read(&path).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default(); + let Some(u) = users.iter_mut().find(|u| u.name == name) else { + eprintln!("no user {}", name); + std::process::exit(1); + }; + u.salt = random_hex(16); + u.hash = hash_password(password, &u.salt); + std::fs::write(&path, serde_json::to_vec_pretty(&users).unwrap_or_default()).expect("write users.json"); + let spath = dir.join("sessions.json"); + let mut sessions: HashMap = std::fs::read(&spath).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default(); + sessions.retain(|_, n| n != name); + let _ = std::fs::write(&spath, serde_json::to_vec(&sessions).unwrap_or_default()); + println!("password of {} set", name); +} + #[tokio::main] async fn main() { let dir = PathBuf::from(std::env::var("SAVE_DIR").unwrap_or_else(|_| "/data".into())); + let args: Vec = std::env::args().collect(); + if args.get(1).map(|s| s.as_str()) == Some("set-password") && args.len() == 4 { + set_password_cli(&dir, &args[2], &args[3]); + return; + } tokio::fs::create_dir_all(dir.join("users")).await.expect("data directory"); let users: Vec = tokio::fs::read(dir.join("users.json")).await.ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default(); let sessions: HashMap = tokio::fs::read(dir.join("sessions.json")).await.ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default();