181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import { isAdminSelfRegistrationEnabled, setAdminSelfRegistrationEnabled } from "@cms/db"
|
|
import { Button } from "@cms/ui/button"
|
|
import { revalidatePath } from "next/cache"
|
|
import Link from "next/link"
|
|
import { redirect } from "next/navigation"
|
|
|
|
import { AdminShell } from "@/components/admin-shell"
|
|
import { translateMessage } from "@/i18n/messages"
|
|
import { getAdminMessages, resolveAdminLocale } from "@/i18n/server"
|
|
import { requirePermissionForRoute } from "@/lib/route-guards"
|
|
|
|
type SearchParamsInput = Promise<Record<string, string | string[] | undefined>>
|
|
|
|
function toSingleValue(input: string | string[] | undefined): string | null {
|
|
if (Array.isArray(input)) {
|
|
return input[0] ?? null
|
|
}
|
|
|
|
return input ?? null
|
|
}
|
|
|
|
async function requireSettingsPermission() {
|
|
await requirePermissionForRoute({
|
|
nextPath: "/settings",
|
|
permission: "users:manage_roles",
|
|
scope: "global",
|
|
})
|
|
}
|
|
|
|
async function getSettingsTranslator() {
|
|
const locale = await resolveAdminLocale()
|
|
const messages = await getAdminMessages(locale)
|
|
|
|
return (key: string, fallback: string) => translateMessage(messages, key, fallback)
|
|
}
|
|
|
|
async function updateRegistrationPolicyAction(formData: FormData) {
|
|
"use server"
|
|
|
|
await requireSettingsPermission()
|
|
const t = await getSettingsTranslator()
|
|
const enabled = formData.get("enabled") === "on"
|
|
|
|
try {
|
|
await setAdminSelfRegistrationEnabled(enabled)
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : ""
|
|
const normalizedMessage = errorMessage.toLowerCase()
|
|
const isDatabaseUnavailable = errorMessage.includes("P1001")
|
|
const isSchemaMissing =
|
|
errorMessage.includes("P2021") ||
|
|
normalizedMessage.includes("system_setting") ||
|
|
normalizedMessage.includes("does not exist")
|
|
|
|
const userMessage = isDatabaseUnavailable
|
|
? t(
|
|
"settings.registration.errors.databaseUnavailable",
|
|
"Saving settings failed. The database is currently unreachable.",
|
|
)
|
|
: isSchemaMissing
|
|
? t(
|
|
"settings.registration.errors.schemaMissing",
|
|
"Saving settings failed. Apply the latest database migrations and try again.",
|
|
)
|
|
: t(
|
|
"settings.registration.errors.updateFailed",
|
|
"Saving settings failed. Ensure database migrations are applied.",
|
|
)
|
|
|
|
redirect(`/settings?error=${encodeURIComponent(userMessage)}`)
|
|
}
|
|
|
|
revalidatePath("/settings")
|
|
revalidatePath("/register")
|
|
redirect(
|
|
`/settings?notice=${encodeURIComponent(
|
|
t("settings.registration.success.updated", "Registration policy updated."),
|
|
)}`,
|
|
)
|
|
}
|
|
|
|
export default async function SettingsPage({ searchParams }: { searchParams: SearchParamsInput }) {
|
|
const role = await requirePermissionForRoute({
|
|
nextPath: "/settings",
|
|
permission: "users:manage_roles",
|
|
scope: "global",
|
|
})
|
|
|
|
const [params, locale, isRegistrationEnabled] = await Promise.all([
|
|
searchParams,
|
|
resolveAdminLocale(),
|
|
isAdminSelfRegistrationEnabled(),
|
|
])
|
|
const messages = await getAdminMessages(locale)
|
|
const t = (key: string, fallback: string) => translateMessage(messages, key, fallback)
|
|
|
|
const notice = toSingleValue(params.notice)
|
|
const error = toSingleValue(params.error)
|
|
|
|
return (
|
|
<AdminShell
|
|
role={role}
|
|
activePath="/settings"
|
|
badge={t("settings.badge", "Admin Settings")}
|
|
title={t("settings.title", "Settings")}
|
|
description={t(
|
|
"settings.description",
|
|
"Manage runtime policies for the admin authentication and onboarding flow.",
|
|
)}
|
|
actions={
|
|
<Link
|
|
href="/"
|
|
className="inline-flex rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium hover:bg-neutral-100"
|
|
>
|
|
{t("settings.actions.backToDashboard", "Back to dashboard")}
|
|
</Link>
|
|
}
|
|
>
|
|
{notice ? (
|
|
<section className="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
|
{notice}
|
|
</section>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<section className="rounded-xl border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-800">
|
|
{error}
|
|
</section>
|
|
) : null}
|
|
|
|
<section className="rounded-xl border border-neutral-200 p-6">
|
|
<div className="space-y-5">
|
|
<div className="space-y-2">
|
|
<h2 className="text-xl font-medium">
|
|
{t("settings.registration.title", "Admin self-registration")}
|
|
</h2>
|
|
<p className="text-sm text-neutral-600">
|
|
{t(
|
|
"settings.registration.description",
|
|
"When enabled, /register can create additional admin accounts after initial owner bootstrap.",
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="rounded-lg border border-neutral-200 p-4 text-sm text-neutral-700">
|
|
<p>
|
|
{t("settings.registration.currentStatusLabel", "Current status")}:{" "}
|
|
<strong>
|
|
{isRegistrationEnabled
|
|
? t("settings.registration.status.enabled", "Enabled")
|
|
: t("settings.registration.status.disabled", "Disabled")}
|
|
</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<form action={updateRegistrationPolicyAction} className="space-y-4">
|
|
<label className="flex items-center gap-3 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
name="enabled"
|
|
defaultChecked={isRegistrationEnabled}
|
|
className="h-4 w-4 rounded border-neutral-300"
|
|
/>
|
|
<span>
|
|
{t(
|
|
"settings.registration.checkboxLabel",
|
|
"Allow self-registration on /register for admin users",
|
|
)}
|
|
</span>
|
|
</label>
|
|
|
|
<Button type="submit">
|
|
{t("settings.registration.actions.save", "Save registration policy")}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
</AdminShell>
|
|
)
|
|
}
|