This commit is contained in:
2026-10-23 10:23:03 -04:00
parent 84b363b863
commit c4d9d1b86c
10 changed files with 625 additions and 22 deletions
+28 -7
View File
@@ -6,7 +6,7 @@ Inspired by Reddit's [The Button](https://en.wikipedia.org/wiki/The_Button_(Redd
## How to play
1. Sign up with a name and password (or log back in).
1. Sign in with your eom.dev account through Dex. New accounts are registered in OpenLDAP from the eom.dev account portal.
2. Press the button. The countdown resets to 24 hours.
3. You score points based on the zone the countdown was in when you pressed. The longer you wait, the more you score, but if everyone waits too long, the game ends.
@@ -25,7 +25,7 @@ Higher-value zones are shorter, so scoring big is risky.
Every press records the zone, the points, and the exact remaining time in milliseconds. Players can view their own score, presses per zone, and press history at any time. A public live leaderboard shows all players ranked by total points, with press counts; ties are sorted alphabetically. Scores refresh every five seconds and immediately after you press or join. Detailed global stats (presses per zone, closest call, full press log) stay hidden until the game ends.
Logged-in players can also join the mailing list.
Logged-in players can also join the separate email-only mailing list; that subscription does not require an LDAP/Dex account.
The Chat section is a simple public chatroom. Logged-in players can post plain-text messages (up to 200 characters, one message every two seconds). The latest 100 messages are displayed oldest first and refresh every five seconds while Chat is open. Messages persist in SQLite across restarts, including after the game ends. Administrators can remove messages through the Admin dashboard; players cannot edit messages.
@@ -63,7 +63,7 @@ The database fields `players.is_admin`, `players.is_banned`, and `players.is_del
Account deletion is **not** a comprehensive personal-data erasure tool: the separate mailing-list CSV, backups, exported data, and third-party copies are not edited automatically. Handle those separately when necessary. Audit identifiers and anonymized game history are deliberately retained. Dashboard controls refresh manually to avoid moving focused actions unexpectedly; access is still rechecked by the server each time. No game reset/end controls are included.
Admin access uses the existing bearer-token authentication, with cross-site fetch rejection on admin mutations and no permissive CORS. Use HTTPS behind a reverse proxy before exposing the service; administrators should log out on shared devices. The existing login failure limits apply, but there is no separate MFA or password reconfirmation flow yet.
Admin access uses the existing per-game session authorization, with cross-site request rejection when Dex mode is enabled and no permissive CORS. In Dex mode the OIDC flow establishes a game session only after server-side token validation; its opaque token is held in an HttpOnly, SameSite cookie and is not exposed to JavaScript, a URL fragment, or localStorage. POST requests require an exact same-origin `Origin`. Use HTTPS behind a reverse proxy before exposing the service; administrators should log out on shared devices. Local-login failure limits apply only when OIDC is disabled; require MFA at Dex for privileged identities.
Admin API: authenticated admin-only `GET /api/admin`; `POST /api/admin/action` accepts `{action, target_id, value?}` for `set_admin`, `set_banned`, and `delete_player`, or `{action: "delete_message", target_id, at}`. Boolean flags must be actual JSON booleans; IDs/timestamps must match the selected record. Anonymous requests receive 401, non-admin accounts 403, invalid inputs 400, missing targets 404, and protected actions 409. All JSON responses use `Cache-Control: no-store`.
@@ -189,7 +189,7 @@ docker compose down -v # stop and delete all
### Without Docker
Requires Python 3.9+. No third-party packages.
Requires Python 3.9+. Install `requirements.txt` before running. OIDC mode uses Authlib, requests, and JOSE verification; install the dependencies even when local development temporarily runs without Dex.
```sh
python server.py
@@ -219,6 +219,22 @@ Set via environment variables (in Docker, add them under `environment:` in `comp
| `BUTTON_DATA_FILE` | `data.json` | Legacy JSON file to import if no saved SQLite game exists. |
| `BUTTON_DATABASE_FILE` | `data.sqlite3` | SQLite game database; defaults to `BUTTON_DATA_FILE` with a `.sqlite3` suffix. |
| `BUTTON_MAILING_LIST_FILE` | `mailing_list.csv` | Mailing list file. |
| `BUTTON_OIDC_ISSUER` | unset | Dex issuer URL (HTTPS). Enables OIDC mode when configured. |
| `BUTTON_OIDC_CLIENT_ID` | unset | Dex OIDC client ID registered for The Button. |
| `BUTTON_OIDC_CLIENT_SECRET` | unset | Client secret; provide through a private deployment secret, not Git. |
| `BUTTON_OIDC_REDIRECT_URI` | unset | Exact HTTPS callback URI registered in Dex, e.g. `https://button.eom.dev/auth/oidc/callback`. |
| `BUTTON_OIDC_REQUIRED_GROUP` | unset | Required Dex `groups` claim value for The Button access. Required in OIDC mode. |
| `BUTTON_OIDC_ADMIN_GROUP` | unset | Optional Dex group granting The Button administrator role. |
| `BUTTON_OIDC_SUBJECT_LINKS_JSON` | `{}` | Temporary map from OIDC `sub` values to existing player IDs, preserving game history. |
| `BUTTON_OIDC_ADMIN_SUBJECTS_JSON` | `[]` | Optional explicit Dex subject allowlist for bootstrap administrators. |
| `BUTTON_OIDC_REGISTER_URL` | account portal URL | Link to account registration. |
| `BUTTON_OIDC_COOKIE_SECURE` | `1` | Mark the OIDC state cookie Secure; disable only for isolated local tests. |
Copy `.env.example` to a private deployment environment and register the exact callback URI with Dex. Request `openid`, `profile`, `email`, and `groups` scopes; configure Dex to emit a stable `sub`, verified email, a usable `preferred_username`, and the required service group. OIDC mode disables `/api/register` and `/api/login`; when OIDC is not configured, the legacy local login remains available for development.
Use Dex `sub` as the immutable identity key. Do not automatically link a local player just because an email or display name matches. For score/history continuity, obtain the correct Dex subject through a trusted admin process, map it to that existing player ID with `BUTTON_OIDC_SUBJECT_LINKS_JSON`, have the player sign in once, then remove the temporary mapping after `oidc_sub` is saved. Never replace player IDs or rewrite press/season history. A name collision without an explicit subject link is rejected rather than silently merging accounts.
The Button validates the issuer, audience, signature (RS256), expiry, nonce, verified-email claim, and required group. Dex groups gate access; The Button retains its own scores, bans, and administrator roles. Only map the Button's admin group/subjects to administrators. Protect Dex/LDAP and callback traffic with TLS, keep the client secret private, and test sign-in, role changes, and revocation before rollout. Existing local accounts are not automatically migrated or deleted.
Tip: `BUTTON_DURATION_SECONDS=60` makes for quick testing.
@@ -241,19 +257,24 @@ Tip: `BUTTON_DURATION_SECONDS=60` makes for quick testing.
| GET | `/api/stats` | | Global stats (403 until the game ends) |
| POST | `/api/subscribe` | ✓ | `{first_name, email, last_name?, phone?}` |
Authenticated requests use `Authorization: Bearer <token>`.
In local-login mode, authenticated requests use `Authorization: Bearer <token>`. In Dex mode, the browser uses the server-issued HttpOnly session cookie instead; JavaScript does not receive the bearer token.
## Security notes
- Passwords are hashed with PBKDF2-SHA256 (600k iterations); session tokens are stored only as SHA-256 hashes.
- Legacy local passwords are hashed with PBKDF2-SHA256 (600k iterations); session tokens are stored only as SHA-256 hashes.
- Logins lock for 15 minutes after 5 failed attempts per name.
- With Dex configured, legacy local login/register endpoints are disabled. New Button players are linked to stable Dex `sub` values; passwords remain in OpenLDAP and are never copied to The Button. Existing local hashes remain for migration/rollback until explicitly retired.
- Configure `BUTTON_OIDC_REQUIRED_GROUP` to a Dex group backed by the appropriate OpenLDAP `groupOfUniqueNames`. The account portal updates that group only after confirming the registration email.
- The account portal accepts only allowlisted service targets. After email confirmation, the password is sent via LDAP Password Modify over verified TLS; slapd generates the hash. Production provisioning is gated on `LDAP_EXPECTED_PASSWORD_SCHEME={ARGON2}`. This is an operator acknowledgement, not a live read of `cn=config`; independently verify the server's Argon2 module and `olcPasswordHash` setting.
- Do not use MD5-crypt, `{CRYPT}`, MD5, or cleartext for new accounts. Current osixia OpenLDAP v2 documentation includes Argon2 support/defaults; v1 may require a deliberate data/config migration before enabling account provisioning.
- Use `BUTTON_OIDC_SUBJECT_LINKS_JSON` only for explicit one-time legacy-account linking. Do not link by matching email or display name alone; a name collision without an administrator-approved subject mapping is rejected to protect player scores.
- The server speaks plain HTTP. **Put it behind a TLS-terminating reverse proxy** (e.g. Caddy or nginx) before exposing it to the internet.
- Registration and mailing-list signup are not rate limited.
## Project layout
```
server.py Game server and API (Python standard library only)
server.py Game server and API
static/ Frontend (HTML, CSS, vanilla JS)
Dockerfile Container image
compose.yaml Docker Compose service and data volume