Skip to content

B-AUTH-01 — Sign Up · Magic Link Login · Post-Login Dashboard Stub

SURFACE: rtopacks.com.au — auth flow + account dashboard
CLOUDFLARE ACCOUNT: e5a9830215a8d88961dc6c80a8c7442a (UCCA — do not touch UCCO)
DO NOT TOUCH: All enrichment workers, D1 enrichment tables, trust surface, engine-db
DB: rtopacks-db D1 334ac8fb-9850-48c0-9da0-b56c55640e98
EMAIL: Resend (already integrated for keys.ucca.online — use same provider + admin@ucca.com.au sender)
BRIEF DRIP RULE: Deploy and confirm before next brief.


Why this exists

The DEWR NTR working group email goes out Monday. DEWR will click through to rtopacks.com.au. The "Sign in to run this analysis on your scope" CTA on the homepage and the Sign up journey circle on the qual page currently go nowhere. That is the only thing that needs to be fixed before Monday. Everything else is polish — this is the gate.


Scope — what this brief delivers

  1. Schema — two new tables in rtopacks-db
  2. Auth API — three edge function endpoints
  3. UI/auth page (sign up + sign in, same page), magic link landing page, post-login dashboard stub
  4. Wiring — homepage CTA → /auth, qual page Sign up circle → /auth, session-aware nav

This is not a full account system. It is the minimum viable gate that makes the product feel real to a new visitor. Composer, scope analysis, cp.rtopacks.com.au — all future briefs. This brief just opens the door.


Schema — add to rtopacks-db

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  last_login TEXT,
  source TEXT DEFAULT 'signup'
);

CREATE TABLE IF NOT EXISTS magic_tokens (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  token TEXT NOT NULL UNIQUE,
  user_id INTEGER NOT NULL REFERENCES users(id),
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  expires_at TEXT NOT NULL,
  used INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_magic_tokens_token ON magic_tokens(token);
CREATE INDEX IF NOT EXISTS idx_magic_tokens_user_id ON magic_tokens(user_id);

Session storage: Cloudflare KV — namespace RTOPACKS_SESSIONS. Key: session:{uuid}, Value: JSON {user_id, email, created_at}. TTL: 30 days.


Auth API — three endpoints

POST /api/auth/request

Request a magic link.

Body: { "email": "user@example.com" }

Logic: 1. Validate email format 2. Upsert into users table — if email exists, fetch existing user_id; if new, insert and get new user_id 3. Generate a cryptographically random token — crypto.randomUUID() is fine 4. Set expires_at = now + 15 minutes 5. Insert into magic_tokens 6. Send magic link email via Resend: - From: RTOpacks <noreply@rtopacks.com.au> - Subject: Sign in to RTOpacks - Body: clean, minimal — "Click to sign in: [link]". Link: https://rtopacks.com.au/auth/verify?token={token} - No HTML bloat — plain but styled is fine 7. Return { success: true } — never confirm or deny if email exists

Rate limit: Max 3 requests per email per hour — check magic_tokens count where created_at > now - 1 hour

GET /api/auth/verify?token={token}

Verify the magic link and create session.

Logic: 1. Look up token in magic_tokens where token = ? AND used = 0 AND expires_at > datetime('now') 2. If not found or expired: redirect to /auth?error=expired 3. If valid: - Mark token as used = 1 - Update users.last_login = datetime('now') - Generate session UUID - Store in KV: RTOPACKS_SESSIONS.set('session:{uuid}', JSON.stringify({user_id, email}), {expirationTtl: 2592000}) (30 days) - Set cookie: rtopacks_session={uuid}; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/ - Redirect to /account

POST /api/auth/logout

Clear session.

Logic: 1. Read rtopacks_session cookie 2. Delete from KV: RTOPACKS_SESSIONS.delete('session:{session_id}') 3. Clear cookie 4. Redirect to /


Session middleware

Create a getSession(request) helper used across all auth-required pages:

async function getSession(request, env) {
  const cookie = request.headers.get('Cookie') || '';
  const match = cookie.match(/rtopacks_session=([^;]+)/);
  if (!match) return null;
  const sessionId = match[1];
  const data = await env.RTOPACKS_SESSIONS.get(`session:${sessionId}`);
  if (!data) return null;
  return JSON.parse(data);
}

Returns null if no valid session, or {user_id, email} if valid.


UI — three pages/states

/auth — Sign up / Sign in page

Single page, single email input. No separate sign up vs sign in flows — email is the identifier, the system handles both.

Design: - Full viewport, same video world background as qual page (use hero-10 — business/professional) - Frosted glass card centred, ~420px wide - RTOpacks logo/wordmark top of card - Headline: "Sign in or create your account" - Subtext: "Enter your email — we'll send you a link. No password needed." - Email input (large, teal focus ring, matches search pill DNA) - Submit button: "Send link →" (teal, full width) - After submit: swap form for confirmation: "Check your inbox — link sent to {email}" with a small resend option after 60 seconds - Error state: if ?error=expired in URL, show "That link has expired. Enter your email to get a new one." - Footer: "By signing in you agree to our Terms · Privacy Policy" (links to trust surface)

/auth/verify — handled server-side (redirect only, no UI needed)

/account — Post-login dashboard stub

This is the Level 3 surface. For Monday it is a stub — real enough to land on, not embarrassing.

Design: - Same HUD aesthetic as qual page — single viewport, video background, dark overlay - Top bar: RTOpacks logo left, {email} + Sign out link right - Main content area: - Welcome message: "Welcome to RTOpacks, {email}." - Subheadline: "Your qualification intelligence dashboard is being set up." - One CTA card: "Explore qualifications →" links back to homepage search - One teaser card (locked/coming soon): "Scope analysis — see how your existing scope maps to the full training package landscape." — muted, with a lock icon. This signals what's coming without pretending it exists. - Footer: same attribution as qual page

Session check: If no valid session cookie, redirect to /auth.


Homepage: - "Sign in to run this analysis on your scope →" link → /auth - If session exists: swap this to "Go to your account →"/account

Qual page journey circles: - Sign up circle (currently ghost) → /auth - If session exists: circle becomes active teal, links to /account

Global nav (if there is one): add Sign in / Account link


Ops stub required (OPS-AS-OS rule)

Add to ops.rtopacks.com.au: - Auth API/api/auth/request · /api/auth/verify · /api/auth/logout · status: LIVE - KV namespaceRTOPACKS_SESSIONS · last write: [timestamp] - Users table — count: [n] users · last signup: [timestamp]


KV namespace

Create KV namespace RTOPACKS_SESSIONS in the UCCA Cloudflare account (e5a9830215a8d88961dc6c80a8c7442a). Bind to the rtopacks Next.js app Worker.


Keep it simple. No heavy HTML. Matches RTOpacks brand:

Subject: Sign in to RTOpacks

Hi,

Click the link below to sign in to RTOpacks. This link expires in 15 minutes.

→ Sign in: https://rtopacks.com.au/auth/verify?token={token}

If you didn't request this, ignore this email.

— RTOpacks
United Central Colleges of Australia Pty Ltd
rtopacks.com.au

Deploy checklist

  • Schema migrations run — users and magic_tokens tables created in rtopacks-db
  • KV namespace RTOPACKS_SESSIONS created and bound
  • POST /api/auth/request — test with real email, confirm link arrives at admin@ucca.com.au
  • GET /api/auth/verify — click link, confirm redirect to /account with session cookie set
  • POST /api/auth/logout — confirm cookie cleared, redirect to /
  • /auth page — renders correctly, form submits, confirmation state shows
  • /account page — redirects to /auth if no session, renders dashboard stub if session valid
  • Homepage CTA wired to /auth
  • Qual page Sign up circle wired to /auth
  • Session-aware nav — Sign in / Account switches correctly
  • Ops stubs added to ops.rtopacks.com.au
  • Test full flow end-to-end: land on qual page → click Sign up circle → /auth → enter email → receive link → click → /account → Sign out → back to homepage

Test email to use: admin@ucca.com.au


What this does NOT build

  • Scope analysis (future brief)
  • RTO profile / onboarding flow (future brief)
  • cp.rtopacks.com.au Level 3 full build (future brief)
  • Billing / subscription (future brief)
  • Social auth / OAuth (not needed at this stage)