Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan, deploye og oppdatere .md-filer for deltaker-tilgang-runden
Bygge og scratch-verifisere sekundær e-postadresse (migrasjon 017)
Bygge HCP-historikk over tid
Sekundær e-postadresse (det enkle tilfellet) er bygget og scratch-verifisert (20/20 sjekker), test_isolation.sql fortsatt 12/12.
Hva er bygget:
Migrasjon 017_secondary_email.sql — to nye tabeller (secondary_email_token, user_secondary_email), samme bevis-eierskap-token-mønster som ADR-032.
Backend: POST /auth/secondary-email (send bekreftelseslenke), POST /auth/secondary-email/confirm (bekreft, ingen sesjon påkrevd), DELETE /auth/secondary-email/{id}. Viktigst: verify_magic_link og login_with_password løser nå en sekundær adresse til den eksisterende eierens konto i stedet for å opprette en ny, separat konto — dette er selve poenget med funksjonen.
Frontend: ny seksjon "Andre e-postadresser" i /account (ikke dashbordet — se merknad under), /verify-email håndterer nå både e-postbytte og ny-sekundær-adresse via en ?kind=secondary-parameter.
Én bevisst plasseringsavgjørelse jeg tok uten å spørre: du sa opprinnelig at hele multi-e-post-flyten burde skje "fra dashboard-siden". Jeg la likevel dette i /account, fordi jeg her kun bygger det enkle tilfellet (fri adresse) — konsistent med hvor e-postbytte (ADR-032) allerede ligger. Når/hvis den harde saken (ekte konto-sammenslåing, der data faktisk "dukker opp") bygges senere, er dashbordet trolig riktigere siden gevinsten vises der. Si fra hvis du vil at den skal flyttes allerede nå.
Verifisert grundig: ny sekundær-adresse legges IKKE til før bekreftet; token kan ikke gjenbrukes; adresse som allerede er en annens hovedadresse ELLER en annens sekundæradresse avvises tydelig; innlogging (magic-link OG passord) via sekundæradressen løses korrekt til samme, eksisterende konto; en fremmed kan ikke slette andres sekundæradresse; og — kritisk — etter sletting oppretter en ny innlogging på den adressen en helt ny, separat konto (beviser fjerningen er reell).
Ingen migrasjon kjørt mot ekte teecup_db ennå.
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import Link from "next/link"
|
|
import { useSearchParams } from "next/navigation"
|
|
import { CheckCircle2, XCircle } from "lucide-react"
|
|
|
|
type Status = "verifying" | "success" | "error"
|
|
|
|
export function VerifyEmailForm() {
|
|
const searchParams = useSearchParams()
|
|
const token = searchParams.get("token")
|
|
// ?kind=secondary (satt av send_secondary_email_verification, app/email.py)
|
|
// skiller "legg til ekstra adresse" fra det vanlige "bytt hovedadresse"-
|
|
// løpet -- samme sides UI, ulikt endepunkt/budskap.
|
|
const isSecondary = searchParams.get("kind") === "secondary"
|
|
const [status, setStatus] = useState<Status>(token ? "verifying" : "error")
|
|
const [email, setEmail] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
setError("Mangler kode i lenken.")
|
|
return
|
|
}
|
|
async function verify() {
|
|
try {
|
|
const res = await fetch(isSecondary ? "/auth/secondary-email/confirm" : "/auth/profile/email/confirm", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({ token }),
|
|
})
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => null)
|
|
throw new Error(body?.detail?.message ?? "Lenken er ugyldig eller utløpt.")
|
|
}
|
|
if (!isSecondary) {
|
|
const result: { email: string } = await res.json()
|
|
setEmail(result.email)
|
|
}
|
|
setStatus("success")
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.")
|
|
setStatus("error")
|
|
}
|
|
}
|
|
void verify()
|
|
}, [token, isSecondary])
|
|
|
|
if (status === "verifying") {
|
|
return (
|
|
<div className="flex w-full flex-col items-center gap-4 rounded-3xl border border-border bg-card p-8 text-center shadow-lg shadow-black/5">
|
|
<div
|
|
aria-hidden="true"
|
|
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
|
|
/>
|
|
<p className="text-sm text-muted-foreground" role="status">
|
|
Bekrefter ny e-postadresse …
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === "success") {
|
|
return (
|
|
<div className="flex w-full flex-col items-center gap-5 rounded-3xl border border-border bg-card p-6 text-center shadow-lg shadow-black/5 sm:p-8">
|
|
<div className="flex size-16 items-center justify-center rounded-2xl bg-primary/15">
|
|
<CheckCircle2 aria-hidden="true" className="size-8 text-primary" />
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="text-2xl font-extrabold tracking-tight text-balance">
|
|
{isSecondary ? "Adresse lagt til" : "E-post oppdatert"}
|
|
</h2>
|
|
{isSecondary ? (
|
|
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
|
Adressen er nå knyttet til kontoen din — du kan logge inn med den i tillegg til
|
|
hovedadressen.
|
|
</p>
|
|
) : (
|
|
email && (
|
|
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
|
Du logger nå inn med <span className="font-semibold text-foreground">{email}</span>.
|
|
</p>
|
|
)
|
|
)}
|
|
</div>
|
|
<Link
|
|
href="/account"
|
|
className="text-sm font-semibold text-primary transition-colors hover:text-primary/80"
|
|
>
|
|
Til kontoinnstillinger
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex w-full flex-col items-center gap-5 rounded-3xl border border-border bg-card p-6 text-center shadow-lg shadow-black/5 sm:p-8">
|
|
<div className="flex size-16 items-center justify-center rounded-2xl bg-destructive/15">
|
|
<XCircle aria-hidden="true" className="size-8 text-destructive" />
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="text-2xl font-extrabold tracking-tight text-balance">Kunne ikke bekrefte</h2>
|
|
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">{error}</p>
|
|
</div>
|
|
<Link
|
|
href="/account"
|
|
className="text-sm font-semibold text-primary transition-colors hover:text-primary/80"
|
|
>
|
|
Til kontoinnstillinger
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|