diff --git a/.gitignore b/.gitignore
index 1c1d1dd..3d90451 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/target
+/sim-admin-password
/.NOCOMMIT
sim.db
sim.db-shm
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 792a208..3727325 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Notable changes to this project are documented here.
### Added
+- Add an HTTP Basic Auth-protected `/admin` path with a random password persisted in the app data volume.
- Add an About dialog explaining the simulation, shards, actors, and persistence.
- Docker Compose runs the gateway and each shard in its own container, with one persistent SQLite volume per shard.
- Public clients are view-only: the UI hides editing controls and the server rejects remote writes; localhost clients retain editing access.
diff --git a/Cargo.lock b/Cargo.lock
index 1804ea6..e027a31 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2798,6 +2798,7 @@ name = "sim"
version = "0.1.0"
dependencies = [
"axum",
+ "base64",
"include_dir",
"picard",
"rand 0.8.8",
@@ -2805,6 +2806,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
+ "subtle",
"tokio",
"tracing",
"tracing-subscriber",
diff --git a/Cargo.toml b/Cargo.toml
index 5b19a7c..90c828b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2024"
members = ["ui"]
[workspace.dependencies]
+base64 = "0.22"
+subtle = "2.6"
tracing = "0.1"
tungstenite = "0.29"
uuid = { version = "1", features = ["v4", "serde"] }
@@ -17,6 +19,8 @@ console = ["picard/console", "serde"]
serde = ["picard/serde"]
[dependencies]
+base64.workspace = true
+subtle.workspace = true
rune = "0.14.2"
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
diff --git a/Justfile b/Justfile
index bd7eb28..64db2b8 100644
--- a/Justfile
+++ b/Justfile
@@ -7,5 +7,8 @@ run:
stop:
docker compose down --remove-orphans
+admin:
+ docker compose exec -T app cat /data/admin-password
+
dev:
SHARD_DATABASE_URL="sqlite://sim-shard-{shard}.db?mode=rwc" cargo run
\ No newline at end of file
diff --git a/README.md b/README.md
index 69dd88a..654b700 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ People, animals and carts that cross a shard border are migrated with their whol
Clients outside localhost can view the simulation but cannot edit it: the UI hides editing controls, and the server rejects remote write requests and WebSocket edit commands. When exposing the app through a reverse proxy, keep the published port bound to host loopback and ensure the proxy sets `X-Forwarded-For` or `X-Real-IP`. Compose enables `SIM_TRUST_PROXY_HEADERS=1` for this setup. Docker-host requests to `localhost` remain editable, while forwarded requests are classified by their client IP.
+The `/admin` path on the same host and port provides authenticated editing. On first start, the app generates a random password and persists it in the `app_data` Docker volume; it stays the same across container restarts. The default username is `admin` (override with `SIM_ADMIN_USER`). Run `just admin` to print the password from the running app container. The normal `/` routes remain view-only. Keep the admin route behind HTTPS through Caddy because Basic Auth credentials are only encoded, not encrypted, without TLS. Removing volumes with `docker compose down -v` also removes this password, so the next start generates a new one.
+
## Economy
Every shard founds markets and workplaces: farms, woodcutters, quarries, hunters, toolmakers and cartwrights. Adults are hired, commute to their job site each day, work their shift, sell the goods at a market, get paid and go home. Farmers sow fields that ripen into crops; gatherers fell trees, quarry stone and hunt; crafters buy materials and make tools and carts. Markets are actors with their own coins and stock; prices follow supply and demand, and buyers and sellers haggle. Merchants carry surplus goods to markets on other shards. Markets, workplaces, carts and crop fields are drawn on the map, and the side panel shows each person's job and coins.
diff --git a/compose.yaml b/compose.yaml
index cf3a8a8..a2c17aa 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -9,8 +9,11 @@ services:
SIM_SHARD_ADDRS: shard-0:9000,shard-1:9000,shard-2:9000
SIM_CLUSTER_TOKEN: ${SIM_CLUSTER_TOKEN:-compose-dev-token}
SIM_TRUST_PROXY_HEADERS: "1"
+ SIM_ADMIN_USER: ${SIM_ADMIN_USER:-admin}
+ SIM_ADMIN_PASSWORD_FILE: /data/admin-password
volumes:
- ./config.rn:/etc/sim/config.rn:ro
+ - app_data:/data
ports:
- "127.0.0.1:7878:7878"
depends_on:
@@ -65,6 +68,7 @@ services:
restart: unless-stopped
volumes:
+ app_data:
shard_0_data:
shard_1_data:
shard_2_data:
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 147a7ff..7f03e2f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,15 +4,21 @@ mod world;
use axum::Router;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
-use axum::extract::{ConnectInfo, DefaultBodyLimit, Path, Request, State};
+use axum::extract::{ConnectInfo, DefaultBodyLimit, OriginalUri, Path, Request, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::{Html, IntoResponse, Json, Response};
use axum::routing::{get, post};
+use base64::Engine;
+use base64::engine::general_purpose::STANDARD as BASE64;
use cluster::gateway::{GatewayOptions, Msg, Outputs};
use cluster::shard::ShardOptions;
use include_dir::{Dir, include_dir};
+use rand::Rng;
+use std::fs::{self, OpenOptions};
+use std::io::{self, Write};
use std::net::{IpAddr, SocketAddr};
+use std::path::{Path as FilePath, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot, watch};
@@ -25,10 +31,20 @@ static UI: Dir = include_dir!("$OUT_DIR/ui-pkg");
const INDEX: &str = r#"