import { eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { beginTwoFactor, confirmTwoFactor, disableTwoFactor, } from "@/actions/twofactor"; import { ContentCard } from "@/components/public/ui"; import { env } from "@/env"; import { auth } from "@/lib/auth"; import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter"; import { totpKeyUri } from "@/lib/auth/totp"; import { db, User } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; // TODO: Cache Components adoption. Refactor this route so this opt-out can be removed. // See: https://nextjs.org/docs/app/guides/migrating-to-cache-components export const instant = false; export default async function TwoFactorPage({ searchParams, }: { searchParams: Promise<{ error?: string; enabled?: string; disabled?: string; }>; }) { const t = await getTranslations("pages.settings2fa"); const session = await auth(); if (!session?.user?.id) redirect("/login"); const sp = await searchParams; const id = Number(session.user.id); let user: { twoFactorSecret: string | null; twoFactorConfirmedAt: Date | null; twoFactorRecoveryCodes: string | null; } | null = null; try { const [row] = await db .select({ twoFactorSecret: User.twoFactorSecret, twoFactorConfirmedAt: User.twoFactorConfirmedAt, twoFactorRecoveryCodes: User.twoFactorRecoveryCodes, }) .from(User) .where(eq(User.id, id)) .limit(1); user = row ?? null; } catch { user = null; } const hasAppKey = Boolean(env.APP_KEY); const enabled = Boolean(user?.twoFactorConfirmedAt); const pending = Boolean(user?.twoFactorSecret && !user?.twoFactorConfirmedAt); const hotelName = await resolveHotelName(); let secret = ""; let uri = ""; let recoveryCodes: string[] = []; if (pending && hasAppKey && user?.twoFactorSecret) { try { secret = new LaravelEncrypter(env.APP_KEY as string).decrypt( user.twoFactorSecret, ); uri = totpKeyUri(secret, session.user.name ?? "user", hotelName); if (user.twoFactorRecoveryCodes) { recoveryCodes = JSON.parse(user.twoFactorRecoveryCodes) as string[]; } } catch { secret = ""; } } return (

{t("backToSettings")}

{sp.enabled ? ( <>

{t("nowEnabled")}

Recovery Codes

Store these one-time use codes in a safe place. Each can be used once if you lose access to your authenticator app.

{recoveryCodes.map((code) => ( {code} ))}
) : null} {sp.disabled ? (

{t("disabledNotice")}

) : null} {sp.error === "badcode" ? (

{t("badCode")}

) : null} {sp.error === "ratelimit" ? (

{t("rateLimit")}

) : null} {!hasAppKey ? (

{t("noAppKeyPrefix")} APP_KEY {t("noAppKeyMiddle")} APP_KEY {t("noAppKeySuffix")}

) : enabled ? ( <>

{t("isEnabled")} {t("onYourAccount")}

) : pending ? ( <>

{t("scanOrEnter")}

{t("scanInstructions")}

{t("manualKey")}{" "} {secret}

{uri}

) : ( <>

{t("enableIntro")}

)}
); }