Skip to content

About

Which team sits where at the Hackathon?

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

BaselHack Rooms

A small web app that shows the Basel SBB (memox) floor plan and which hackathon team sits in which room.

  • Visitors get a public, read-only page: click a room to see its teams, or browse the searchable assignment table underneath the plan.
  • Organizers manage teams through a REST API with a shared API key, using the bundled bhrooms CLI.
  • A team can be in exactly one room; a room can hold many teams.

Stack

Layer Choice
Backend Rust, axum 0.8, tokio, sqlx (SQLite), tower-http
Frontend React + TypeScript + Vite + Tailwind CSS, hand-authored SVG
CLI Rust bhrooms binary, clap + reqwest
Deploy Single Rust binary in an OCI image, Caddy for TLS, Podman Compose on a Fedora VPS

Repository layout

Cargo.toml            workspace (server + cli)
server/               bhrooms-server: JSON API, SQLite, embedded SPA
  migrations/         schema
  tests/api.rs        endpoint integration tests
cli/                  bhrooms CLI client
web/                  React SPA (Vite, Tailwind, Vitest)
deploy/               Dockerfile, compose, Caddyfile, build/deploy/remote scripts
scripts/dev.sh        run server + Vite together
DESIGN.md             design document
DEPLOY.md             build → package → deploy guide

Local development

Prerequisites: Rust (1.75+), Node 20+, npm. SQLite is bundled through sqlx.

# API on http://localhost:8080
cargo run -p bhrooms-server

# Frontend with hot reload on http://localhost:5173 (proxies /api to :8080)
cd web
npm install
npm run dev

Or run both with scripts/dev.sh.

cargo build --release embeds web/dist into the binary, so run npm run build in web/ first. The Docker build does this automatically.

The server prints a freshly generated API key on every start:

============================================================
 API key: 6f1c...            (keep secret; used by the CLI)
 The key changes on every restart;
 read it again from the logs.
============================================================

The key is held in memory only and the server is the only writer: the CLI never creates or changes it. A restart invalidates it (unless BHROOMS_API_KEY is set). See Security.

CLI quickstart

cargo run -p bhrooms -- rooms list
cargo run -p bhrooms -- teams create "Team Rocket" --size 4 --api-key <key>
cargo run -p bhrooms -- assign "Team Rocket" XL --api-key <key>
cargo run -p bhrooms -- overview
cargo run -p bhrooms -- teams rename "Team Rocket" "Team Phoenix" --api-key <key>
cargo run -p bhrooms -- unassign "Team Phoenix" --api-key <key>
cargo run -p bhrooms -- teams delete "Team Phoenix" --api-key <key>

Team arguments accept an id or an exact name (the id wins when both match). --json prints machine-readable output for every command. Exit codes: 0 success, 1 usage/API error, 2 auth error.

The CLI talks to http://localhost:8080 by default. Point it at another server with --server <url>:

bhrooms --server https://rooms.example.com overview

or persist them so they stick across runs:

bhrooms config set server https://rooms.example.com
bhrooms config set api-key <key>
bhrooms config show      # effective values and where they come from

Precedence: --server/--api-key flags > BHROOMS_SERVER/BHROOMS_API_KEY env vars > persisted config > http://localhost:8080. The config lives at ~/.config/bhrooms/config.toml (or $XDG_CONFIG_HOME/bhrooms/config.toml); the API key is stored there in plaintext with file mode 0600.

Set the key once instead of repeating it:

export BHROOMS_API_KEY=<printed key>
export BHROOMS_SERVER=http://localhost:8080   # optional

The CLI never generates or changes keys. It only carries the one the server printed: --api-key, BHROOMS_API_KEY, or bhrooms config set api-key <key>. To rotate, restart the server and read the new key from its log.

Configuration

Variable Default Purpose
BHROOMS_ADDR 0.0.0.0:8080 Listen address
BHROOMS_DB ./data/rooms.db SQLite file (WAL mode)
BHROOMS_API_KEY generated per start Pin a fixed key at startup
RUST_LOG info Log filter

API

Base path /api, JSON only. Errors use { "error": { "code": "...", "message": "..." } }.

Public:

Method Path Description
GET /api/overview Rooms with teams plus unassigned teams (one shot)
GET /api/rooms Fixed room list
GET /api/teams Teams with their current room
GET /healthz Liveness probe

Admin (Authorization: Bearer <API_KEY>):

Method Path Body Result
POST /api/teams { "name": "...", "size": 4 } 201 team
PATCH /api/teams/:id { "name"?, "size"? } 200 team
DELETE /api/teams/:id – 204
PUT /api/teams/:id/room { "roomId": "L1" | null } 200 team

Validation: unknown room → 400, unknown team → 404, duplicate name → 409, missing/invalid key → 401. size is an integer ≥ 0 or null.

Security

  • Writes require a shared bearer key, compared in constant time (SHA-256 + constant-time digest comparison).
  • The key is generated with a CSPRNG on every start, kept in memory only and printed to the log. Nothing can change it at runtime: the server is the only generator and the only writer, so there is no key-management endpoint.
  • A restart/redeploy invalidates existing keys: read the new one with mise run deploy:key (or podman compose logs app).
  • The web UI is read-only; there are no cookies, sessions or user accounts.

Tests

cargo test --workspace          # server unit + API integration, CLI unit/mock tests
cargo clippy --workspace --all-targets -- -D warnings
cd web && npm test              # Vitest + React Testing Library
cd web && npm run build         # typecheck + production build

Manual smoke checklist

  1. Start the server and open http://localhost:8080 (after npm run build in web/), or use Vite on :5173.
  2. Confirm all 10 rooms render (the safe space and the OK room greyed out with their red/orange outlines) and the table says “No teams yet.”
  3. Create a team via CLI, assign it to L1, refresh the page: the room shows a 1 badge and the team appears in the table and room panel.
  4. Assign a second team to L1: both appear; a room can hold many teams.
  5. Click a table row: the matching room is highlighted and scrolled into view.
  6. Search and room filter behave; unassigned teams are listed separately.
  7. Rename, resize (including clearing size with teams size <team> none) and delete the team; the page reflects it after refresh.
  8. Call a write endpoint without the key: 401; with the key: success.
  9. Restart the server: a new key is printed and the old one is rejected; PUT /api/key is gone and answers 404.

Notes and assumptions

  • The floor plan is a trace of the venue plan (Grundriss_Farben_und_qm_Basel-SBB_.pdf, open question Q1 in DESIGN.md). Room outlines follow the drawn walls; furniture, the atrium, corridors and technical zones are omitted or inert; room geometry lives in web/src/rooms.ts.
  • Page title is “BaselHack — Team Rooms” (open question Q2).
  • The official white BaselHack logo from baselhack.ch is vendored as web/public/logo.webp and used by the header and favicon.
  • Fira Code is vendored under web/public/fonts/ (SIL OFL 1.1, license file included) and loaded via @font-face in web/src/index.css.
  • Rooms are fixed and seeded on every start: S1, S2, S3, S4, M1, M2, M3, L1, L2, XL. The room of the location team is drawn on the plan but is inert: it cannot be selected or assigned.
  • Two rooms are reserved: S1 is the safe space (label Safe Space, red outline) and S2 is labelled OK (orange outline). Both are greyed out like the location team's room. The server marks them assignable: false and rejects any attempt to assign a team to them (400); the web UI does not decide this itself, it only renders the flag.

About

Which team sits where at the Hackathon?

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages