Zum Inhalt springen
ZVVAtlas · Showcase

Command Palette

Schnell durch Atlas navigieren

Auth Flow

Atlas Magic-Link-Flow im Detail. Bewusst NICHT signInWithOtp, sondern custom generateActionLink + zvv-mailer für einheitliches Branding und Reply-To ict@zvv.zh.ch.

5-Step Flow

Step 1User submitted Email

Login-Form → POST /login (Server-Action)

Step 2Pre-Check via Service-Role

user_profiles.email lookup. Falls noAccount → CTA "Konto beantragen"

Step 3generateActionLink (NICHT signInWithOtp)

Supabase auth.admin.generateLink → hashed_token. App baut URL selbst.

Step 4Mail via zvv-mailer

POST /api/newsletters/<slug>/send mit rawHtml + headerTitle.

Step 5User klickt Link → /auth/confirm

verifyOtp({token_hash, type}). Session-Cookie auf App-Domain.

Pre-Check Matrix (Issue #31)

Der Pre-Check verhindert, dass auth.admin.generateLink stillschweigend einen Geist-Account anlegt — was sonst zu einem inkonsistenten Auth-State führt.

EmailexistiertaktivVerhalten
marcel.rapold@zvv.zh.chLink gesendet
old.user@zvv.zh.chGeblockt: inaktives Profil
random@example.comGeblockt: noAccount → CTA Signup

Server-Action Code

app/login/actions.tstsx
'use server'

import { generateActionLink } from '@/lib/auth/generate-action-link'
import { sendNewsletter } from '@/lib/notifications/mailer-client'

export async function requestMagicLink(prev, formData) {
  const email = String(formData.get('email')).trim().toLowerCase()

  // 1. Pre-Check (Issue #31): existiert ein aktiver User?
  const { data: profile } = await db
    .from('user_profiles')
    .select('id, is_active')
    .eq('email', email)
    .maybeSingle()
  if (!profile?.is_active) return { ok: false, noAccount: true, error: '…' }

  // 2. Custom Action-Link (NICHT signInWithOtp!)
  const link = await generateActionLink({ type: 'magiclink', email, next, appBase })

  // 3. Mail über zvv-mailer (für einheitliches ZVV-Branding)
  await sendNewsletter({
    subject: 'Anmelde-Link für die ZVV App',
    message: buildMagicLinkHtml(link.actionLink),
    headerTitle: 'Anmeldung',
    recipients: [{ email }],
  })

  return { ok: true }
}