134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { redirect } from "next/navigation";
|
|
import { ContentCard } from "@/components/public/ui";
|
|
import { auth } from "@/lib/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { env } from "@/env";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Social providers we can link. `available` reflects whether the OAuth app
|
|
// credentials are configured (mirrors src/lib/auth.ts, which only registers a
|
|
// provider when both its id + secret env vars are set). When unavailable the
|
|
// connect button is disabled so we don't bounce the user to a 404 sign-in URL.
|
|
const PROVIDERS = [
|
|
{
|
|
key: "discord",
|
|
label: "Discord",
|
|
icon: "🎮",
|
|
available: Boolean(env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET),
|
|
},
|
|
{
|
|
key: "google",
|
|
label: "Google",
|
|
icon: "🔵",
|
|
available: Boolean(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET),
|
|
},
|
|
] as const;
|
|
|
|
export default async function ConnectionsPage() {
|
|
const t = await getTranslations("pages.connections");
|
|
const session = await auth();
|
|
if (!session?.user?.id) redirect("/login");
|
|
|
|
const id = Number(session.user.id);
|
|
|
|
// social_accounts is a CMS-only table that may not exist in every install
|
|
// (it has no bundled migration). Fall back to "nothing linked" on any error so
|
|
// the page still renders the generic connect buttons.
|
|
let linked: Set<string> = new Set();
|
|
let lookupFailed = false;
|
|
try {
|
|
const rows = await prisma.socialAccounts.findMany({
|
|
where: { userId: BigInt(id) },
|
|
select: { provider: true },
|
|
});
|
|
linked = new Set(rows.map((r) => r.provider.toLowerCase()));
|
|
} catch {
|
|
lookupFailed = true;
|
|
}
|
|
|
|
// After connecting/erroring, NextAuth bounces back here so we land on a known page.
|
|
const callbackUrl = "/settings/connections";
|
|
|
|
return (
|
|
<main style={{ display: "grid", gap: "1.5rem", maxWidth: 620, margin: "0 auto" }}>
|
|
<p className="muted" style={{ margin: 0 }}>
|
|
<Link href="/settings">{t("backToSettings")}</Link>
|
|
</p>
|
|
|
|
<ContentCard
|
|
icon="🔗"
|
|
title={t("title")}
|
|
subtitle={t("subtitle")}
|
|
>
|
|
<p className="muted" style={{ marginTop: 0 }}>
|
|
{t("matchByEmail")}{" "}
|
|
<strong>{session.user.name}</strong>.
|
|
</p>
|
|
|
|
{lookupFailed ? (
|
|
<p className="muted" style={{ marginBottom: 0 }}>
|
|
{t("lookupFailed")}
|
|
</p>
|
|
) : null}
|
|
</ContentCard>
|
|
|
|
<div className="card-grid sm-2">
|
|
{PROVIDERS.map((p) => {
|
|
const isLinked = linked.has(p.key);
|
|
return (
|
|
<ContentCard
|
|
key={p.key}
|
|
icon={p.icon}
|
|
title={p.label}
|
|
subtitle={
|
|
!p.available
|
|
? t("statusUnavailable")
|
|
: isLinked
|
|
? t("statusConnected")
|
|
: t("statusNotConnected")
|
|
}
|
|
>
|
|
{isLinked ? (
|
|
<span
|
|
className="btn btn-outline"
|
|
style={{ cursor: "default", pointerEvents: "none" }}
|
|
aria-disabled="true"
|
|
>
|
|
{t("connected")}
|
|
</span>
|
|
) : p.available ? (
|
|
// Plain link into NextAuth's built-in sign-in handler. Re-using
|
|
// the same OAuth flow as /login binds the provider to the
|
|
// matching hotel account (see auth.ts signIn callback).
|
|
<a
|
|
className="btn btn-primary"
|
|
href={`/api/auth/signin/${p.key}?callbackUrl=${encodeURIComponent(
|
|
callbackUrl,
|
|
)}`}
|
|
>
|
|
{t("connectProvider", { provider: p.label })}
|
|
</a>
|
|
) : (
|
|
<span
|
|
className="btn"
|
|
style={{ cursor: "not-allowed", opacity: 0.55, pointerEvents: "none" }}
|
|
aria-disabled="true"
|
|
>
|
|
{t("unavailable")}
|
|
</span>
|
|
)}
|
|
</ContentCard>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<p className="muted" style={{ margin: 0 }}>
|
|
{t("footerNote")}
|
|
</p>
|
|
</main>
|
|
);
|
|
}
|