feat(landing): self-serve registration page via Drasl API (B1)

Add a static /register page (en/es/eu) that calls the Drasl REST API v2
directly from the browser: register -> login -> optional skin upload, all
in the pixel design. Guests never touch Drasl's own web UI.

Drasl config gains the pieces this needs:
- CORSAllowOrigins scoped to the apex (the API has no CORS until set;
  never "*").
- [RateLimit] for the now public-facing anonymous POST /users.
- [RegistrationNewPlayer] with RequireInvite driven by a new
  REGISTRATION_MODE (invite|open) .env flag.

REGISTRATION_MODE has two consumers from one key: render-config.sh derives
the TOML boolean for Drasl (drasl is TOML-only, no env), and the landing
build reads it to show/hide the invite-code field. render-config.sh halts
on any value other than invite/open.

Security verified against drasl source: anonymous POST /users cannot set
privileged fields (isAdmin/isLocked/chosenUuid/maxPlayerCount are gated on
callerIsAdmin in CreateUser), so browser-direct registration is safe.

Docs: plan/16-landing-registration.md captures the design + the B1 vs fork
decision; build-order, deploy, and the deploy skill wire REGISTRATION_MODE
and the landing-rebuild requirement (www/ is gitignored, not updated by a
git pull).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:55:51 +02:00
parent 067e990306
commit df8ffca53e
11 changed files with 766 additions and 11 deletions

View File

@@ -4,6 +4,10 @@
// BASE_DOMAIN is read at build time; falls back to ulicraft.local.
const BASE_DOMAIN = process.env.BASE_DOMAIN ?? "ulicraft.local";
// REGISTRATION_MODE (invite|open) is read at build time, same .env as Drasl's
// render-config.sh. "invite" => the register form must collect an invite code.
const REGISTRATION_MODE = process.env.REGISTRATION_MODE ?? "invite";
// Minecraft / pack facts, surfaced in a few places.
const MC_VERSION = "1.21.1";
@@ -12,6 +16,11 @@ export const site = {
baseDomain: BASE_DOMAIN,
mcVersion: MC_VERSION,
// Register form behaviour, driven by REGISTRATION_MODE in .env.
// true => show the invite-code field (Drasl RequireInvite = true)
// false => open self-registration
registrationRequiresInvite: REGISTRATION_MODE === "invite",
// Design knobs (replace the design's in-browser TweaksPanel). Baked at build.
// mood: "grass" | "nether" | "end"
// hero: "centered" | "split" | "spotlight"
@@ -30,14 +39,24 @@ export const site = {
// Drasl auth web UI + authlib-injector API root (public HTTPS).
authUrl: `https://auth.${BASE_DOMAIN}`,
authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`,
// Drasl REST API v2 root — the /register form calls this from the browser.
// Requires CORSAllowOrigins to include the apex in drasl config.toml.
draslApiUrl: `https://auth.${BASE_DOMAIN}/drasl/api/v2`,
// packwiz modpack entrypoint (public HTTPS, pack. subdomain).
packwizUrl: `https://pack.${BASE_DOMAIN}/pack.toml`,
// Static server-list panel (Status section). No fake live count — see plan
// 09-landing.md option B.
// NMSR avatar renderer root. Player heads come from OUR Drasl skins, not
// Mojang/Crafatar — head URL is `${avatarUrl}/headiso/<uuid>?size=64`.
avatarUrl: `https://avatar.${BASE_DOMAIN}`,
// Live server card (ServerCard.astro). `slots` is the SSG fallback cap shown
// before/without JS; live count comes from mcstatus.io pinging the public
// address. mcstatus.io sets Access-Control-Allow-Origin:* → direct browser
// fetch, no proxy/key. Uptime Kuma stays the history + alerting backend.
status: {
slots: 10,
statusApi: `https://api.mcstatus.io/v2/status/java/${BASE_DOMAIN}`,
},
// Honest stat tiles. Keys (numbers/versions) are language-neutral; labels