137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { redirect } from "next/navigation";
|
|
import { beginTwoFactor, confirmTwoFactor, disableTwoFactor } from "@/actions/twofactor";
|
|
import { ContentCard } from "@/components/public/ui";
|
|
import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter";
|
|
import { totpKeyUri } from "@/lib/auth/totp";
|
|
import { auth } from "@/lib/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
import { env } from "@/env";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
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 } | null = null;
|
|
try {
|
|
user = await prisma.user.findUnique({
|
|
where: { id },
|
|
select: { twoFactorSecret: true, twoFactorConfirmedAt: true },
|
|
});
|
|
} catch {
|
|
user = null;
|
|
}
|
|
|
|
const hasAppKey = Boolean(env.APP_KEY);
|
|
const enabled = Boolean(user?.twoFactorConfirmedAt);
|
|
const pending = Boolean(user?.twoFactorSecret && !user?.twoFactorConfirmedAt);
|
|
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
|
|
|
|
let secret = "";
|
|
let uri = "";
|
|
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);
|
|
} catch {
|
|
secret = "";
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main style={{ display: "grid", gap: "1.5rem", maxWidth: 560, margin: "0 auto" }}>
|
|
<p className="muted" style={{ margin: 0 }}>
|
|
<Link href="/settings">{t("backToSettings")}</Link>
|
|
</p>
|
|
|
|
<ContentCard
|
|
icon="🔒"
|
|
title={t("title")}
|
|
subtitle={t("subtitle")}
|
|
>
|
|
{sp.enabled ? (
|
|
<p style={{ color: "var(--color-accent)", marginTop: 0 }}>{t("nowEnabled")}</p>
|
|
) : null}
|
|
{sp.disabled ? (
|
|
<p className="muted" style={{ marginTop: 0 }}>
|
|
{t("disabledNotice")}
|
|
</p>
|
|
) : null}
|
|
{sp.error === "badcode" ? (
|
|
<p style={{ color: "var(--color-danger)", marginTop: 0 }}>
|
|
{t("badCode")}
|
|
</p>
|
|
) : null}
|
|
{sp.error === "ratelimit" ? (
|
|
<p style={{ color: "var(--color-danger)", marginTop: 0 }}>
|
|
{t("rateLimit")}
|
|
</p>
|
|
) : null}
|
|
|
|
{!hasAppKey ? (
|
|
<p style={{ margin: 0 }}>
|
|
{t("noAppKeyPrefix")} <code>APP_KEY</code> {t("noAppKeyMiddle")}
|
|
<code> APP_KEY</code> {t("noAppKeySuffix")}
|
|
</p>
|
|
) : enabled ? (
|
|
<>
|
|
<p style={{ marginTop: 0 }}>
|
|
<strong>{t("isEnabled")}</strong> {t("onYourAccount")}
|
|
</p>
|
|
<form action={disableTwoFactor} style={{ display: "flex", gap: "0.5rem" }}>
|
|
<input name="code" placeholder={t("codePlaceholder")} inputMode="numeric" required />
|
|
<button type="submit" className="btn btn-danger">
|
|
{t("disable")}
|
|
</button>
|
|
</form>
|
|
</>
|
|
) : pending ? (
|
|
<>
|
|
<h3 style={{ marginTop: 0 }}>{t("scanOrEnter")}</h3>
|
|
<p className="muted" style={{ marginTop: 0 }}>
|
|
{t("scanInstructions")}
|
|
</p>
|
|
<p>
|
|
{t("manualKey")} <code style={{ userSelect: "all" }}>{secret}</code>
|
|
</p>
|
|
<p className="muted" style={{ wordBreak: "break-all", fontSize: "0.8rem" }}>
|
|
{uri}
|
|
</p>
|
|
<form
|
|
action={confirmTwoFactor}
|
|
style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}
|
|
>
|
|
<input name="code" placeholder={t("codePlaceholder")} inputMode="numeric" required />
|
|
<button type="submit" className="btn btn-primary">
|
|
{t("confirm")}
|
|
</button>
|
|
</form>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p style={{ marginTop: 0 }}>
|
|
{t("enableIntro")}
|
|
</p>
|
|
<form action={beginTwoFactor}>
|
|
<button type="submit" className="btn btn-primary">
|
|
{t("enable")}
|
|
</button>
|
|
</form>
|
|
</>
|
|
)}
|
|
</ContentCard>
|
|
</main>
|
|
);
|
|
}
|