feat: two-factor authentication (TOTP + backup codes)
Uses better-auth's built-in twoFactor plugin rather than hand-rolling TOTP — adds the two_factors table and users.twoFactorEnabled, wires the server/client plugins (allowPasswordless: true so OAuth-only accounts aren't locked out of managing 2FA), and adds a custom rate limit for the verify endpoints (5/min — far stricter than the default 100/10s, since a 6-digit code has a much smaller keyspace than a password). New Settings → Security section walks through enable (password confirm -> QR + backup codes -> confirm a live code before it's actually turned on, per the plugin's default flow) and disable. New /verify-2fa page handles the post-password mid-login step, with a backup-code fallback. Verified live end-to-end: enable, confirm, forced re-auth on next sign-in, TOTP accepted, backup code accepted (single-use), disable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth/server";
|
||||
import { db, accounts, users, eq, and } from "@epicure/db";
|
||||
import { SecurityForm } from "@/components/settings/security-form";
|
||||
|
||||
export const metadata: Metadata = {};
|
||||
@@ -9,5 +10,16 @@ export default async function SecurityPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) return null;
|
||||
|
||||
return <SecurityForm currentEmail={session.user.email} />;
|
||||
const [dbUser, credentialAccount] = await Promise.all([
|
||||
db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { twoFactorEnabled: true } }),
|
||||
db.query.accounts.findFirst({ where: and(eq(accounts.userId, session.user.id), eq(accounts.providerId, "credential")) }),
|
||||
]);
|
||||
|
||||
return (
|
||||
<SecurityForm
|
||||
currentEmail={session.user.email}
|
||||
twoFactorEnabled={dbUser?.twoFactorEnabled ?? false}
|
||||
hasPassword={!!credentialAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user