"use client" // Kontoinnstillinger (ADR-021): sett/fjern passord (valgfritt tillegg til // magic-link), aktiver/deaktiver 2FA. Frivillig 2FA-oppsett herfra bruker // SAMME TwoFactorSetupForm som den tvungne flyten rett etter innlogging // (forced=false her -- kun tekst-copyen skiller de to). import type React from "react" import { useEffect, useRef, useState } from "react" import Link from "next/link" import { ArrowLeft, Camera, KeyRound, Lock, Mail, ShieldCheck, ShieldOff, User, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TwoFactorSetupForm } from "@/components/two-factor-flow" // ADR-031: personlig profil, ETT sett per konto (app_user), atskilt fra de // org-scopede `player`-radene organisatorer administrerer. type Me = { id: string email: string display_name: string has_password: boolean two_factor_method: "totp" | "email" | null first_name: string | null last_name: string | null birth_date: string | null gender: "m" | "f" | "x" | null handicap_index: number | null home_club: string | null mobile_country_code: string | null mobile_number: string | null avatar_url: string | null } export function AccountSettings() { const [me, setMe] = useState(null) const [loading, setLoading] = useState(true) const [settingUp2fa, setSettingUp2fa] = useState(false) async function loadMe() { try { const res = await fetch("/auth/me", { credentials: "include" }) if (res.ok) setMe(await res.json()) } finally { setLoading(false) } } useEffect(() => { void loadMe() }, []) async function handleDisable2fa() { if (!confirm("Er du sikker på at du vil slå av topartsautentisering?")) return const res = await fetch("/auth/2fa/disable", { method: "POST", credentials: "include" }) if (res.ok) void loadMe() } if (loading) { return (
) } if (!me) return null return (
{settingUp2fa ? (
{ setSettingUp2fa(false) void loadMe() }} />
) : (

Topartsautentisering (2FA)

{me.two_factor_method ? ( <>

Aktivert med{" "} {me.two_factor_method === "totp" ? "autentisator-app" : "engangskode på e-post"} .

) : ( <>

Ikke aktivert. Anbefales for organisasjonseiere/administratorer -- da vil du bli bedt om å sette det opp ved neste innlogging uansett.

)}
)}
) } // --- Personlig profil (ADR-031) --------------------------------------------- function ProfileSection({ me, onChanged }: { me: Me; onChanged: () => void }) { const [firstName, setFirstName] = useState(me.first_name ?? "") const [lastName, setLastName] = useState(me.last_name ?? "") const [birthDate, setBirthDate] = useState(me.birth_date ?? "") const [gender, setGender] = useState(me.gender ?? "") const [hcp, setHcp] = useState(me.handicap_index === null ? "" : String(me.handicap_index)) const [homeClub, setHomeClub] = useState(me.home_club ?? "") const [mobileCountryCode, setMobileCountryCode] = useState(me.mobile_country_code ?? "+47") const [mobileNumber, setMobileNumber] = useState(me.mobile_number ?? "") const [saving, setSaving] = useState(false) const [uploadingAvatar, setUploadingAvatar] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) const fileInputRef = useRef(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setSaving(true) setError(null) setSuccess(false) try { const res = await fetch("/auth/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ first_name: firstName.trim() === "" ? null : firstName.trim(), last_name: lastName.trim() === "" ? null : lastName.trim(), birth_date: birthDate === "" ? null : birthDate, gender: gender === "" ? null : gender, handicap_index: hcp.trim() === "" ? null : Number(hcp.replace(",", ".")), home_club: homeClub.trim() === "" ? null : homeClub.trim(), mobile_country_code: mobileNumber.trim() === "" ? null : mobileCountryCode.trim(), mobile_number: mobileNumber.trim() === "" ? null : mobileNumber.trim(), }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "Klarte ikke å lagre profilen.") } setSuccess(true) onChanged() } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setSaving(false) } } async function handleAvatarSelected(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return setUploadingAvatar(true) setError(null) try { const formData = new FormData() formData.append("file", file) const res = await fetch("/auth/profile/avatar", { method: "POST", credentials: "include", body: formData, }) if (!res.ok) throw new Error("Klarte ikke å laste opp profilbildet.") onChanged() } catch (err) { setError(err instanceof Error ? err.message : "Klarte ikke å laste opp profilbildet.") } finally { setUploadingAvatar(false) if (fileInputRef.current) fileInputRef.current.value = "" } } async function handleRemoveAvatar() { setUploadingAvatar(true) try { const res = await fetch("/auth/profile/avatar", { method: "DELETE", credentials: "include" }) if (res.ok) onChanged() } finally { setUploadingAvatar(false) } } return (

Personlig profil

{me.avatar_url ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
{me.avatar_url && ( )}
setFirstName(e.target.value)} className="h-11 rounded-xl" />
setLastName(e.target.value)} className="h-11 rounded-xl" />
setBirthDate(e.target.value)} className="h-11 rounded-xl" />
setHcp(e.target.value)} className="h-11 rounded-xl" />
setHomeClub(e.target.value)} className="h-11 rounded-xl" />
setMobileCountryCode(e.target.value)} placeholder="+47" className="h-11 w-20 shrink-0 rounded-xl text-center" /> setMobileNumber(e.target.value)} placeholder="912 34 567" className="h-11 flex-1 rounded-xl" />
{error &&

{error}

} {success &&

Profilen er oppdatert.

}
) } // --- E-post (identifikatoren) ----------------------------------------------- // Bevisst IKKE en del av ProfileSection sin vanlige PATCH -- e-post er // innloggings-identifikatoren, endring krever at den NYE adressen beviser // eierskap først (se app/routers/auth.py sin request_email_change/ // confirm_email_change). function EmailSection({ email }: { email: string }) { const [editing, setEditing] = useState(false) const [newEmail, setNewEmail] = useState("") const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const [sent, setSent] = useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setSubmitting(true) setError(null) try { const res = await fetch("/auth/profile/email", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ new_email: newEmail.trim() }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "Klarte ikke å sende bekreftelseslenken.") } setSent(true) } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setSubmitting(false) } } return (

E-post

Dette er identifikatoren du logger inn med: {email}

{sent ? (

Sjekk innboksen til {newEmail.trim()} — åpne lenken der for å fullføre byttet. Adressen endres ikke før den er bekreftet.

) : editing ? (
setNewEmail(e.target.value)} className="h-12 rounded-xl" />
) : ( )} {error &&

{error}

}
) } function PasswordSection({ hasPassword, onChanged }: { hasPassword: boolean; onChanged: () => void }) { const [password, setPassword] = useState("") const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (password.length < 8 || submitting) return setSubmitting(true) setError(null) setSuccess(false) try { const res = await fetch("/auth/set-password", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ password }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "Klarte ikke å sette passordet.") } setPassword("") setSuccess(true) onChanged() } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setSubmitting(false) } } async function handleRemove() { if (!confirm("Fjerne passordet? Du kan fortsatt logge inn med magic-link.")) return const res = await fetch("/auth/remove-password", { method: "POST", credentials: "include" }) if (res.ok) onChanged() } return (

Passord

{hasPassword ? "Du har et passord satt. Du kan fortsatt logge inn med magic-link når som helst." : "Ikke satt -- du logger i dag kun inn med magic-link. Passord er et valgfritt tillegg, aldri påkrevd."}

setPassword(e.target.value)} className="h-12 rounded-xl" />
{error &&

{error}

} {success &&

Passordet er oppdatert.

} {hasPassword && ( )}
) }