Skip to content

About

Website for dåre

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dare-website

Dåre's website. TanStack Start + Tailwind v4 + shadcn/ui (Base UI, Lyra, Mist)

  • Drizzle + Postgres, with login through Datasektionen SSO. Runs on Bun and deploys to Datasektionen's Nomad cluster.

Getting started

bun install
cp .env.example .env     # then set SESSION_SECRET (openssl rand -base64 32)
bun run services:up      # Postgres + SSO mock (docker compose)
bun run db:migrate
bun run dev              # http://localhost:2027

Logging in locally

docker-compose.yml runs nyckeln-under-dorrmattan, a mock of SSO on http://localhost:7003. Clicking Logga in sends you to its login page. Enter one of the users from dev/nyckeln.yaml:

kth_id Who
turetek admin (dÅrestaben, which has $dare:admin)
nollan regular chapter member
gaest KTH user outside the chapter (guest)

To add users, group members or permissions, edit dev/nyckeln.yaml and run docker compose restart sso.

Auth

  • /auth/login starts an OIDC authorization code flow against OIDC_ISSUER, without PKCE (SSO requires PKCE to be off for the client). /login is a friendlier address for it that lands on ?redirect= (default /dashboard), or goes straight there if already signed in. /auth/callback fetches userinfo and stores the user in an encrypted HttpOnly cookie (dare_session, 7 days). POST /auth/logout clears it. No auth tables live in the database.
  • Anyone with an SSO account can log in. Admins are those with the Hive permission $dare:admin (permission admin in the Hive system dare). SSO includes it in userinfo through the permissions scope, so the app never calls Hive itself. It's read at login, so changes in Hive apply at the next login. The user is { kthid, name, email, isAdmin, isJudge }.
  • Judges may score Jäger vs Minttu and nothing else. Admins add their KTH ids on the Jäger vs Minttu page (battle_judges, changes logged in battle_judge_changes); isJudge is looked up on every request, so it applies right away. Judges can log in with /login like everyone else (the overview sends them on to scoring), or with /domare, which goes straight there.
  • Pages: put protected routes under src/routes/_authed/. context.user is non-null there. Gate admin-only content on context.user.isAdmin (see _authed/dashboard.tsx).
  • Server functions: use .middleware([authMiddleware]) or .middleware([adminMiddleware]) from @/lib/auth/functions (or judgeMiddleware from @/lib/battle/access for scoring). Always check on the server; route guards only affect the UI.

Dashboard

/dashboard has a sidebar (a slide-out menu on phones) with one page per area. Pages under src/routes/_authed/dashboard/_admin/ are for admins only.

Page Who What
Översikt all Summary tiles, quick scoring and recent activity
Profil all Account, permission, theme, log out
Jäger vs Minttu admins, judges Score the battle, who has scored most; admins also manage judges and reset
Biljettsläpp admins The ticket release time the landing page counts down to
Puckopist admins The /game leaderboard: today's numbers, runs to review, hide/approve/remove runs, bans, saving on/off, reset
Aktivitet admins Everything admins have changed, filterable with tabs
Inställningar admins Switch optional features on and off (below)

Features

Parts of the site that are only needed for a while can be switched off under Inställningar. Switched-off features are hidden from the dashboard and the activity log, and the server refuses changes to them. Nothing is deleted. Switches are stored in site_settings and logged in feature_changes.

Feature Default When off
Biljettsläpp on The landing page stays, but without the countdown numbers
Jäger vs Minttu off /battle redirects to the start page
Puckopist on /game redirects to the start page

Profile pictures (avatars) come from /api/avatar/<kthid>, which looks them up in SSO's internal API (SSO_API_URL; SSO gets them from rfinger) and redirects to them. The links expire, so they're only cached in memory for a few hours. Without SSO_API_URL, avatars show initials.

Puckopist (/game) never takes a score from the browser. Before each run the server hands out a single-use ticket (game_runs) with the piste's seed; the game records when jump is pressed and let go, and after the crash sends those presses with an HMAC made with the ticket's key. The server replays them on the same seed (the simulation is deterministic, see src/lib/game/dmath.ts) and saves its own score in game_scores. Runs finished faster than they could be played are refused; runs that don't match their replay, or were played with developer tools open, swapped-out timers or in slow motion, are flagged and (while Granska flaggade åk is on) wait for an admin to approve them. See src/lib/game/anticheat.ts.

Players don't log in, so bans (game_bans) go by an encrypted device cookie, optionally a browser fingerprint or IP (rarely useful, as a whole party shares the same wifi), or words in the name. Shadow bans let the player think their runs are saved. Under Puckopist, admins can also remove runs (logged in game_score_removals), close saving, and reset the leaderboard (only newer runs count; nothing is deleted). Everything else they do there is logged in game_admin_events. All of it shows under Aktivitet.

The ticket release time is stored in site_settings (a single row), and every change is logged in ticket_release_changes. Until an admin sets a time, 2 October 2026 21:00 is used. Times are entered and shown in Swedish time (src/lib/time.ts).

Jäger vs Minttu (/battle)

Off by default. It's only used at the ticket release pub, so switch it on under Inställningar before the pub and off afterwards. While it's off, /battle redirects to the start page, it's hidden from the dashboard and the activity log, and the server refuses scoring. The scores are kept, so start a new round with Nollställ next time.

A full-screen battle for the big screen, based on haaker1/haaker1.github.io (images from there, in public/battle/). The fighters are pushed towards the side that's behind, and every point triggers sparks, a shockwave, screen shake and an impact word. The page has no controls: press F for fullscreen.

  • Admins score only from the Jäger vs Minttu card on /dashboard (made for phones). /battle itself is read-only.
  • The score is in the battle table, and every hit, undo and reset is logged in battle_events.
  • Updates reach every open screen instantly through server-sent events (/api/battle/events), fanned out between app instances with Postgres LISTEN/NOTIFY. Screens also poll every 15 s as a fallback.
  • The screens look after themselves, so nobody has to reload them during the pub: the stream sends the current score on every (re)connect and pings every 15 s. The browser reopens it when pings stop or after an HTTP error (like a 502 during a deploy), backing off up to 15 s. If the page itself couldn't load, it retries every 5 s. While the connection is down, a small Återansluter… shows in a corner. See src/lib/battle/use-battle-stream.ts.

Scripts

Script What it does
dev Vite dev server on port 2027 (fails if taken)
build / start Production build to .output/, serve with Bun
test / test:watch Vitest
check Biome lint + format (writes fixes)
typecheck tsc --noEmit
services:up / services:down Start / stop Postgres and the SSO mock
db:generate Generate a SQL migration from schema changes
db:migrate Apply pending migrations
db:push / db:studio Push schema directly / open Drizzle Studio

Database

Add tables in src/db/schema/ and export them from index.ts. Then run bun run db:generate and bun run db:migrate, and commit the files in drizzle/. In production, the container applies pending migrations on startup (scripts/migrate.ts).

Deployment

Pushing to main runs .github/workflows/deploy.yml, which uses datasektionen/nomad-deploy to build the Dockerfile, push it to ghcr.io/datasektionen/dare-website and run job.nomad.hcl (job dare, namespace default, host dåre.datasektionen.se).

One-time setup, done by D-Sys:

  1. Deploy token: add "dare-website" to the default list in deploy-tokens in infra/github.tf. This creates the repo's NOMAD_TOKEN secret.
  2. Database: create user and database dare on postgres.dsekt.internal.
  3. Hive: create the system dare with a permission admin, and assign it to the group darestaben@datasektionen.se. Members of its subgroups get it too, and only while their membership is active.
  4. SSO client: in SSO's admin panel, create client dare with redirect URI https://xn--dre-ula.datasektionen.se/auth/callback and Hive system dare. Without the Hive system, SSO rejects logins that request permissions. Allow guests if people outside the chapter should be able to log in.
  5. Nomad variables at nomad/jobs/dare: db_password, session_secret (openssl rand -base64 32), oidc_client_secret.
  6. DNS: point dåre.datasektionen.se (xn--dre-ula.datasektionen.se) at the cluster.

About

Website for dåre

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages