summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-09 00:47:35 -0800
committerFuwn <[email protected]>2026-02-09 00:47:35 -0800
commit3473f17b9063ea0ebd2ee93609d6f5750688a0b4 (patch)
tree2e15d1b32e7ccb28d1523da9e4016063e2bd251f /apps
parentformat(worker): Apply Iku formatting (diff)
downloadasa.news-3473f17b9063ea0ebd2ee93609d6f5750688a0b4.tar.xz
asa.news-3473f17b9063ea0ebd2ee93609d6f5750688a0b4.zip
fix: elevate to AAL2 before password/email change when MFA is enabled
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/reader/settings/_components/account-settings.tsx64
1 files changed, 62 insertions, 2 deletions
diff --git a/apps/web/app/reader/settings/_components/account-settings.tsx b/apps/web/app/reader/settings/_components/account-settings.tsx
index 0a8d403..84b8414 100644
--- a/apps/web/app/reader/settings/_components/account-settings.tsx
+++ b/apps/web/app/reader/settings/_components/account-settings.tsx
@@ -24,6 +24,8 @@ export function AccountSettings() {
const [currentPassword, setCurrentPassword] = useState("")
const [newPassword, setNewPassword] = useState("")
const [confirmNewPassword, setConfirmNewPassword] = useState("")
+ const [passwordMfaCode, setPasswordMfaCode] = useState("")
+ const [emailMfaCode, setEmailMfaCode] = useState("")
const [enrolledFactors, setEnrolledFactors] = useState<Factor[]>([])
const [isTotpLoading, setIsTotpLoading] = useState(true)
const [enrollmentState, setEnrollmentState] = useState<EnrollmentState>({ step: "idle" })
@@ -59,13 +61,39 @@ export function AccountSettings() {
},
})
+ async function elevateToAal2(mfaCode: string) {
+ const { data: factorsData } = await supabaseClient.auth.mfa.listFactors()
+ const verifiedFactors = factorsData?.totp.filter((factor) => factor.status === "verified") ?? []
+
+ if (verifiedFactors.length === 0) return
+
+ if (!mfaCode || mfaCode.length !== 6) {
+ throw new Error("enter your 6-digit authenticator code")
+ }
+
+ const { data: challengeData, error: challengeError } =
+ await supabaseClient.auth.mfa.challenge({ factorId: verifiedFactors[0].id })
+
+ if (challengeError) throw new Error("mfa challenge failed: " + challengeError.message)
+
+ const { error: verifyError } = await supabaseClient.auth.mfa.verify({
+ factorId: verifiedFactors[0].id,
+ challengeId: challengeData.id,
+ code: mfaCode,
+ })
+
+ if (verifyError) throw new Error("invalid authenticator code")
+ }
+
const updateEmailAddress = useMutation({
mutationFn: async ({
emailAddress,
password,
+ mfaCode,
}: {
emailAddress: string
password: string
+ mfaCode: string
}) => {
const {
data: { user },
@@ -80,6 +108,8 @@ export function AccountSettings() {
if (signInError) throw new Error("incorrect password")
+ await elevateToAal2(mfaCode)
+
const { error } = await supabaseClient.auth.updateUser({
email: emailAddress,
})
@@ -89,6 +119,7 @@ export function AccountSettings() {
onSuccess: () => {
setNewEmailAddress("")
setEmailPassword("")
+ setEmailMfaCode("")
notify("confirmation email sent to your new address")
},
onError: (error: Error) => {
@@ -100,9 +131,11 @@ export function AccountSettings() {
mutationFn: async ({
currentPassword: current,
newPassword: updated,
+ mfaCode,
}: {
currentPassword: string
newPassword: string
+ mfaCode: string
}) => {
const {
data: { user },
@@ -117,6 +150,8 @@ export function AccountSettings() {
if (signInError) throw new Error("current password is incorrect")
+ await elevateToAal2(mfaCode)
+
const { error } = await supabaseClient.auth.updateUser({
password: updated,
})
@@ -127,6 +162,7 @@ export function AccountSettings() {
setCurrentPassword("")
setNewPassword("")
setConfirmNewPassword("")
+ setPasswordMfaCode("")
notify("password updated — signing out all sessions")
await supabaseClient.auth.signOut({ scope: "global" })
router.push("/sign-in")
@@ -295,7 +331,7 @@ export function AccountSettings() {
event.preventDefault()
const trimmedEmail = newEmailAddress.trim()
if (!trimmedEmail || !emailPassword) return
- updateEmailAddress.mutate({ emailAddress: trimmedEmail, password: emailPassword })
+ updateEmailAddress.mutate({ emailAddress: trimmedEmail, password: emailPassword, mfaCode: emailMfaCode })
}
function handleUpdatePassword(event: React.FormEvent) {
@@ -312,7 +348,7 @@ export function AccountSettings() {
notify("password must be at least 8 characters")
return
}
- updatePassword.mutate({ currentPassword, newPassword })
+ updatePassword.mutate({ currentPassword, newPassword, mfaCode: passwordMfaCode })
}
return (
@@ -383,6 +419,18 @@ export function AccountSettings() {
placeholder="current password"
className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
/>
+ {enrolledFactors.length > 0 && (
+ <input
+ type="text"
+ inputMode="numeric"
+ pattern="[0-9]*"
+ maxLength={6}
+ value={emailMfaCode}
+ onChange={(event) => setEmailMfaCode(event.target.value.replace(/\D/g, ""))}
+ placeholder="authenticator code"
+ className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
+ />
+ )}
<button
type="submit"
disabled={updateEmailAddress.isPending || !newEmailAddress.trim() || !emailPassword}
@@ -416,6 +464,18 @@ export function AccountSettings() {
placeholder="confirm new password"
className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
/>
+ {enrolledFactors.length > 0 && (
+ <input
+ type="text"
+ inputMode="numeric"
+ pattern="[0-9]*"
+ maxLength={6}
+ value={passwordMfaCode}
+ onChange={(event) => setPasswordMfaCode(event.target.value.replace(/\D/g, ""))}
+ placeholder="authenticator code"
+ className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
+ />
+ )}
<button
type="submit"
disabled={updatePassword.isPending || !currentPassword || !newPassword || newPassword !== confirmNewPassword}