feat(admin): add registration policy settings and disabled register state
This commit is contained in:
5
TODO.md
5
TODO.md
@@ -28,7 +28,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
||||
- [x] [P1] Bootstrap first-run owner account creation via initial registration flow
|
||||
- [x] [P1] Enforce invariant: exactly one owner user must always exist
|
||||
- [x] [P1] Create hidden technical support user by default (non-demotable, non-deletable)
|
||||
- [~] [P1] Admin registration policy control (allow/deny self-registration for admin panel)
|
||||
- [x] [P1] Admin registration policy control (allow/deny self-registration for admin panel)
|
||||
- [x] [P1] First-start onboarding route for initial owner creation (`/welcome`)
|
||||
- [x] [P1] Split auth entry points (`/welcome`, `/login`, `/register`) with cross-links
|
||||
- [~] [P2] Support fallback sign-in route (`/support/:key`) as break-glass access
|
||||
@@ -45,7 +45,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
||||
- [x] [P1] Authentication and session model (`admin`, `editor`, `manager`)
|
||||
- [x] [P1] Protected admin routes and session handling
|
||||
- [~] [P1] Temporary admin posts CRUD sandbox for baseline functional validation
|
||||
- [ ] [P1] Core admin IA (pages/media/users/commissions/settings)
|
||||
- [~] [P1] Core admin IA (pages/media/users/commissions/settings)
|
||||
|
||||
### Public App
|
||||
|
||||
@@ -198,6 +198,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
||||
- [2026-02-10] Shared CRUD base (`@cms/crud`) is live with validation, not-found errors, and audit hook contracts; only posts are migrated so far.
|
||||
- [2026-02-10] Admin dashboard includes a temporary posts CRUD sandbox (create/update/delete) to validate the shared CRUD base through the real app UI.
|
||||
- [2026-02-10] Admin i18n baseline now resolves locale from cookie and loads runtime message dictionaries in root layout; admin locale switcher is active on auth and dashboard views.
|
||||
- [2026-02-10] Admin self-registration policy is now managed via `/settings` and persisted in `system_setting`; env var is fallback/default only.
|
||||
|
||||
## How We Use This File
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AdminLocaleSwitcher } from "@/components/admin-locale-switcher"
|
||||
import { useAdminT } from "@/providers/admin-i18n-provider"
|
||||
|
||||
type LoginFormProps = {
|
||||
mode: "signin" | "signup-owner" | "signup-user"
|
||||
mode: "signin" | "signup-owner" | "signup-user" | "signup-disabled"
|
||||
}
|
||||
|
||||
type AuthResponse = {
|
||||
@@ -41,6 +41,7 @@ export function LoginForm({ mode }: LoginFormProps) {
|
||||
const [isBusy, setIsBusy] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
const canSubmitSignUp = mode === "signup-owner" || mode === "signup-user"
|
||||
|
||||
async function handleSignIn(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
@@ -141,7 +142,9 @@ export function LoginForm({ mode }: LoginFormProps) {
|
||||
? t("auth.titles.signIn", "Sign in to CMS Admin")
|
||||
: mode === "signup-owner"
|
||||
? t("auth.titles.signUpOwner", "Welcome to CMS Admin")
|
||||
: t("auth.titles.signUpUser", "Create an admin account")}
|
||||
: mode === "signup-user"
|
||||
? t("auth.titles.signUpUser", "Create an admin account")
|
||||
: t("auth.titles.signUpDisabled", "Registration is disabled")}
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-600">
|
||||
{mode === "signin"
|
||||
@@ -151,7 +154,12 @@ export function LoginForm({ mode }: LoginFormProps) {
|
||||
"auth.descriptions.signUpOwner",
|
||||
"Create the first owner account to initialize this admin instance.",
|
||||
)
|
||||
: t("auth.descriptions.signUpUser", "Self-registration is enabled for admin users.")}
|
||||
: mode === "signup-user"
|
||||
? t("auth.descriptions.signUpUser", "Self-registration is enabled for admin users.")
|
||||
: t(
|
||||
"auth.descriptions.signUpDisabled",
|
||||
"Self-registration is currently turned off by an administrator.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +216,7 @@ export function LoginForm({ mode }: LoginFormProps) {
|
||||
|
||||
{error ? <p className="text-sm text-red-600">{error}</p> : null}
|
||||
</form>
|
||||
) : (
|
||||
) : canSubmitSignUp ? (
|
||||
<form
|
||||
onSubmit={handleSignUp}
|
||||
className="mt-8 space-y-4 rounded-xl border border-neutral-200 p-6"
|
||||
@@ -290,6 +298,20 @@ export function LoginForm({ mode }: LoginFormProps) {
|
||||
{error ? <p className="text-sm text-red-600">{error}</p> : null}
|
||||
{success ? <p className="text-sm text-green-700">{success}</p> : null}
|
||||
</form>
|
||||
) : (
|
||||
<section className="mt-8 space-y-4 rounded-xl border border-neutral-200 p-6">
|
||||
<p className="text-sm text-neutral-700">
|
||||
{t(
|
||||
"auth.messages.registrationDisabled",
|
||||
"Registration is disabled for this admin instance. Ask an administrator to create an account or enable self-registration.",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-600">
|
||||
<Link href={`/login?next=${encodeURIComponent(nextPath)}`} className="underline">
|
||||
{t("auth.links.goToSignIn", "Go to sign in")}
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -200,6 +200,12 @@ export default async function AdminHomePage({
|
||||
>
|
||||
{t("dashboard.actions.openRoadmap", "Open roadmap and progress")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="inline-flex rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium hover:bg-neutral-100"
|
||||
>
|
||||
{t("settings.title", "Settings")}
|
||||
</Link>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default async function RegisterPage({ searchParams }: { searchParams: Sea
|
||||
const enabled = await isSelfRegistrationEnabled()
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/login?next=${encodeURIComponent(nextPath)}`)
|
||||
return <LoginForm mode="signup-disabled" />
|
||||
}
|
||||
|
||||
return <LoginForm mode="signup-user" />
|
||||
|
||||
188
apps/admin/src/app/settings/page.tsx
Normal file
188
apps/admin/src/app/settings/page.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { hasPermission } from "@cms/content/rbac"
|
||||
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 { AdminLocaleSwitcher } from "@/components/admin-locale-switcher"
|
||||
import { translateMessage } from "@/i18n/messages"
|
||||
import { getAdminMessages, resolveAdminLocale } from "@/i18n/server"
|
||||
import { resolveRoleFromServerContext } from "@/lib/access-server"
|
||||
|
||||
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() {
|
||||
const role = await resolveRoleFromServerContext()
|
||||
|
||||
if (!role) {
|
||||
redirect("/login?next=/settings")
|
||||
}
|
||||
|
||||
if (!hasPermission(role, "users:manage_roles", "global")) {
|
||||
redirect("/unauthorized?required=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 }) {
|
||||
await requireSettingsPermission()
|
||||
|
||||
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 (
|
||||
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-8 px-6 py-16">
|
||||
<header className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm uppercase tracking-[0.2em] text-neutral-500">
|
||||
{t("settings.badge", "Admin Settings")}
|
||||
</p>
|
||||
<AdminLocaleSwitcher />
|
||||
</div>
|
||||
<h1 className="text-4xl font-semibold tracking-tight">{t("settings.title", "Settings")}</h1>
|
||||
<p className="text-neutral-600">
|
||||
{t(
|
||||
"settings.description",
|
||||
"Manage runtime policies for the admin authentication and onboarding flow.",
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{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>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -19,11 +19,13 @@ const messages: AdminMessages = {
|
||||
signIn: "Sign in",
|
||||
signUpOwner: "Welcome",
|
||||
signUpUser: "Create account",
|
||||
signUpDisabled: "Registration disabled",
|
||||
},
|
||||
descriptions: {
|
||||
signIn: "Sign in description",
|
||||
signUpOwner: "Owner description",
|
||||
signUpUser: "User description",
|
||||
signUpDisabled: "Disabled description",
|
||||
},
|
||||
fields: {
|
||||
name: "Name",
|
||||
@@ -48,6 +50,7 @@ const messages: AdminMessages = {
|
||||
messages: {
|
||||
ownerCreated: "Owner account created.",
|
||||
accountCreated: "Account created.",
|
||||
registrationDisabled: "Registration is disabled.",
|
||||
},
|
||||
errors: {
|
||||
nameRequired: "Name is required.",
|
||||
@@ -57,6 +60,33 @@ const messages: AdminMessages = {
|
||||
networkSignUp: "Network sign up error",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
badge: "Admin Settings",
|
||||
title: "Settings",
|
||||
description: "Settings description",
|
||||
actions: {
|
||||
backToDashboard: "Back to dashboard",
|
||||
},
|
||||
registration: {
|
||||
title: "Registration",
|
||||
description: "Registration description",
|
||||
currentStatusLabel: "Current status",
|
||||
status: {
|
||||
enabled: "Enabled",
|
||||
disabled: "Disabled",
|
||||
},
|
||||
checkboxLabel: "Allow registration",
|
||||
actions: {
|
||||
save: "Save",
|
||||
},
|
||||
success: {
|
||||
updated: "Updated",
|
||||
},
|
||||
errors: {
|
||||
updateFailed: "Update failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
badge: "Admin App",
|
||||
title: "Content Dashboard",
|
||||
|
||||
@@ -43,6 +43,13 @@ const guardRules: GuardRule[] = [
|
||||
scope: "global",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: /^\/settings(?:\/|$)/,
|
||||
requirement: {
|
||||
permission: "users:manage_roles",
|
||||
scope: "global",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: /^\/(?:$|\?)/,
|
||||
requirement: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeRole, type Role } from "@cms/content/rbac"
|
||||
import { db } from "@cms/db"
|
||||
import { db, isAdminSelfRegistrationEnabled } from "@cms/db"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma"
|
||||
import { toNextJsHandler } from "better-auth/next-js"
|
||||
@@ -43,8 +43,7 @@ export async function isInitialOwnerRegistrationOpen(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function isSelfRegistrationEnabled(): Promise<boolean> {
|
||||
// Temporary fallback until registration policy is managed from admin settings.
|
||||
return process.env.CMS_ADMIN_SELF_REGISTRATION_ENABLED === "true"
|
||||
return isAdminSelfRegistrationEnabled()
|
||||
}
|
||||
|
||||
export async function canUserSelfRegister(): Promise<boolean> {
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
"titles": {
|
||||
"signIn": "Bei CMS Admin anmelden",
|
||||
"signUpOwner": "Willkommen bei CMS Admin",
|
||||
"signUpUser": "Admin-Konto erstellen"
|
||||
"signUpUser": "Admin-Konto erstellen",
|
||||
"signUpDisabled": "Registrierung ist deaktiviert"
|
||||
},
|
||||
"descriptions": {
|
||||
"signIn": "Better Auth ist in dieser App über /api/auth aktiv.",
|
||||
"signUpOwner": "Erstelle das erste Owner-Konto, um diese Admin-Instanz zu initialisieren.",
|
||||
"signUpUser": "Selbstregistrierung für Admin-Benutzer ist aktiviert."
|
||||
"signUpUser": "Selbstregistrierung für Admin-Benutzer ist aktiviert.",
|
||||
"signUpDisabled": "Selbstregistrierung wurde von einer Administratorin oder einem Administrator deaktiviert."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
@@ -42,7 +44,8 @@
|
||||
},
|
||||
"messages": {
|
||||
"ownerCreated": "Owner-Konto erstellt. Registrierung ist jetzt deaktiviert.",
|
||||
"accountCreated": "Konto erstellt."
|
||||
"accountCreated": "Konto erstellt.",
|
||||
"registrationDisabled": "Für diese Admin-Instanz ist die Registrierung deaktiviert. Bitte wende dich an eine Administratorin oder einen Administrator."
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Name ist für die Kontoerstellung erforderlich",
|
||||
@@ -52,6 +55,33 @@
|
||||
"networkSignUp": "Netzwerkfehler bei der Registrierung"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"badge": "Admin-Einstellungen",
|
||||
"title": "Einstellungen",
|
||||
"description": "Verwalte Laufzeitrichtlinien für Authentifizierung und Onboarding im Admin-Bereich.",
|
||||
"actions": {
|
||||
"backToDashboard": "Zurück zum Dashboard"
|
||||
},
|
||||
"registration": {
|
||||
"title": "Admin-Selbstregistrierung",
|
||||
"description": "Wenn aktiviert, können über /register nach der initialen Owner-Erstellung weitere Admin-Konten erstellt werden.",
|
||||
"currentStatusLabel": "Aktueller Status",
|
||||
"status": {
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert"
|
||||
},
|
||||
"checkboxLabel": "Selbstregistrierung auf /register für Admin-Benutzer erlauben",
|
||||
"actions": {
|
||||
"save": "Registrierungsrichtlinie speichern"
|
||||
},
|
||||
"success": {
|
||||
"updated": "Registrierungsrichtlinie aktualisiert."
|
||||
},
|
||||
"errors": {
|
||||
"updateFailed": "Speichern der Einstellungen fehlgeschlagen. Stelle sicher, dass Datenbankmigrationen angewendet wurden."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"badge": "Admin-App",
|
||||
"title": "Content-Dashboard",
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
"titles": {
|
||||
"signIn": "Sign in to CMS Admin",
|
||||
"signUpOwner": "Welcome to CMS Admin",
|
||||
"signUpUser": "Create an admin account"
|
||||
"signUpUser": "Create an admin account",
|
||||
"signUpDisabled": "Registration is disabled"
|
||||
},
|
||||
"descriptions": {
|
||||
"signIn": "Better Auth is active on this app via /api/auth.",
|
||||
"signUpOwner": "Create the first owner account to initialize this admin instance.",
|
||||
"signUpUser": "Self-registration is enabled for admin users."
|
||||
"signUpUser": "Self-registration is enabled for admin users.",
|
||||
"signUpDisabled": "Self-registration is currently turned off by an administrator."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
@@ -42,7 +44,8 @@
|
||||
},
|
||||
"messages": {
|
||||
"ownerCreated": "Owner account created. Registration is now disabled.",
|
||||
"accountCreated": "Account created."
|
||||
"accountCreated": "Account created.",
|
||||
"registrationDisabled": "Registration is disabled for this admin instance. Ask an administrator to create an account or enable self-registration."
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Name is required for account creation",
|
||||
@@ -52,6 +55,33 @@
|
||||
"networkSignUp": "Network error while signing up"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"badge": "Admin Settings",
|
||||
"title": "Settings",
|
||||
"description": "Manage runtime policies for the admin authentication and onboarding flow.",
|
||||
"actions": {
|
||||
"backToDashboard": "Back to dashboard"
|
||||
},
|
||||
"registration": {
|
||||
"title": "Admin self-registration",
|
||||
"description": "When enabled, /register can create additional admin accounts after initial owner bootstrap.",
|
||||
"currentStatusLabel": "Current status",
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"checkboxLabel": "Allow self-registration on /register for admin users",
|
||||
"actions": {
|
||||
"save": "Save registration policy"
|
||||
},
|
||||
"success": {
|
||||
"updated": "Registration policy updated."
|
||||
},
|
||||
"errors": {
|
||||
"updateFailed": "Saving settings failed. Ensure database migrations are applied."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"badge": "Admin App",
|
||||
"title": "Content Dashboard",
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
"titles": {
|
||||
"signIn": "Iniciar sesión en CMS Admin",
|
||||
"signUpOwner": "Bienvenido a CMS Admin",
|
||||
"signUpUser": "Crear una cuenta de admin"
|
||||
"signUpUser": "Crear una cuenta de admin",
|
||||
"signUpDisabled": "El registro está deshabilitado"
|
||||
},
|
||||
"descriptions": {
|
||||
"signIn": "Better Auth está activo en esta app mediante /api/auth.",
|
||||
"signUpOwner": "Crea la primera cuenta owner para inicializar esta instancia de administración.",
|
||||
"signUpUser": "El registro automático está habilitado para usuarios admin."
|
||||
"signUpUser": "El registro automático está habilitado para usuarios admin.",
|
||||
"signUpDisabled": "El auto-registro está desactivado actualmente por un administrador."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nombre",
|
||||
@@ -42,7 +44,8 @@
|
||||
},
|
||||
"messages": {
|
||||
"ownerCreated": "Cuenta owner creada. El registro ahora está deshabilitado.",
|
||||
"accountCreated": "Cuenta creada."
|
||||
"accountCreated": "Cuenta creada.",
|
||||
"registrationDisabled": "El registro está deshabilitado para esta instancia de administración. Pide a un administrador que cree una cuenta o habilite el auto-registro."
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "El nombre es obligatorio para crear la cuenta",
|
||||
@@ -52,6 +55,33 @@
|
||||
"networkSignUp": "Error de red al registrarse"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"badge": "Ajustes de Admin",
|
||||
"title": "Ajustes",
|
||||
"description": "Gestiona políticas de ejecución para autenticación y onboarding del panel admin.",
|
||||
"actions": {
|
||||
"backToDashboard": "Volver al panel"
|
||||
},
|
||||
"registration": {
|
||||
"title": "Auto-registro de admin",
|
||||
"description": "Cuando está habilitado, /register puede crear cuentas admin adicionales después del bootstrap inicial del owner.",
|
||||
"currentStatusLabel": "Estado actual",
|
||||
"status": {
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Deshabilitado"
|
||||
},
|
||||
"checkboxLabel": "Permitir auto-registro en /register para usuarios admin",
|
||||
"actions": {
|
||||
"save": "Guardar política de registro"
|
||||
},
|
||||
"success": {
|
||||
"updated": "Política de registro actualizada."
|
||||
},
|
||||
"errors": {
|
||||
"updateFailed": "No se pudieron guardar los ajustes. Asegúrate de que las migraciones de base de datos estén aplicadas."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"badge": "App Admin",
|
||||
"title": "Panel de Contenido",
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
"titles": {
|
||||
"signIn": "Se connecter à CMS Admin",
|
||||
"signUpOwner": "Bienvenue sur CMS Admin",
|
||||
"signUpUser": "Créer un compte admin"
|
||||
"signUpUser": "Créer un compte admin",
|
||||
"signUpDisabled": "L’inscription est désactivée"
|
||||
},
|
||||
"descriptions": {
|
||||
"signIn": "Better Auth est actif sur cette application via /api/auth.",
|
||||
"signUpOwner": "Créez le premier compte owner pour initialiser cette instance d’administration.",
|
||||
"signUpUser": "L’auto-inscription est activée pour les utilisateurs admin."
|
||||
"signUpUser": "L’auto-inscription est activée pour les utilisateurs admin.",
|
||||
"signUpDisabled": "L’auto-inscription est actuellement désactivée par un administrateur."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nom",
|
||||
@@ -42,7 +44,8 @@
|
||||
},
|
||||
"messages": {
|
||||
"ownerCreated": "Compte owner créé. L’inscription est maintenant désactivée.",
|
||||
"accountCreated": "Compte créé."
|
||||
"accountCreated": "Compte créé.",
|
||||
"registrationDisabled": "L’inscription est désactivée pour cette instance admin. Demandez à un administrateur de créer un compte ou de réactiver l’auto-inscription."
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Le nom est requis pour créer un compte",
|
||||
@@ -52,6 +55,33 @@
|
||||
"networkSignUp": "Erreur réseau lors de l’inscription"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"badge": "Paramètres Admin",
|
||||
"title": "Paramètres",
|
||||
"description": "Gérez les politiques d’exécution pour l’authentification et l’onboarding de l’admin.",
|
||||
"actions": {
|
||||
"backToDashboard": "Retour au tableau de bord"
|
||||
},
|
||||
"registration": {
|
||||
"title": "Auto-inscription admin",
|
||||
"description": "Lorsqu’elle est activée, /register peut créer des comptes admin supplémentaires après l’initialisation du premier owner.",
|
||||
"currentStatusLabel": "Statut actuel",
|
||||
"status": {
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé"
|
||||
},
|
||||
"checkboxLabel": "Autoriser l’auto-inscription sur /register pour les utilisateurs admin",
|
||||
"actions": {
|
||||
"save": "Enregistrer la politique d’inscription"
|
||||
},
|
||||
"success": {
|
||||
"updated": "Politique d’inscription mise à jour."
|
||||
},
|
||||
"errors": {
|
||||
"updateFailed": "Échec de l’enregistrement des paramètres. Vérifiez que les migrations de base de données sont appliquées."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"badge": "Application Admin",
|
||||
"title": "Tableau de bord contenu",
|
||||
|
||||
@@ -39,6 +39,7 @@ Optional:
|
||||
|
||||
- Support user bootstrap is available via `bun run auth:seed:support`.
|
||||
- Root `bun run db:seed` runs DB seed and support-user seed.
|
||||
- `CMS_ADMIN_SELF_REGISTRATION_ENABLED` is temporary until admin settings UI manages this policy.
|
||||
- `CMS_ADMIN_SELF_REGISTRATION_ENABLED` is now a fallback/default only.
|
||||
- Runtime source of truth is admin settings (`/settings`) backed by `system_setting`.
|
||||
- Owner/support checks for future admin user-management mutations remain tracked in `TODO.md`.
|
||||
- Email verification and forgot/reset password pipelines are tracked for MVP2.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "system_setting" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "system_setting_pkey" PRIMARY KEY ("key")
|
||||
);
|
||||
@@ -87,3 +87,12 @@ model Verification {
|
||||
@@index([identifier])
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
model SystemSetting {
|
||||
key String @id
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("system_setting")
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ export {
|
||||
registerPostCrudAuditHook,
|
||||
updatePost,
|
||||
} from "./posts"
|
||||
export { isAdminSelfRegistrationEnabled, setAdminSelfRegistrationEnabled } from "./settings"
|
||||
|
||||
56
packages/db/src/settings.ts
Normal file
56
packages/db/src/settings.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { db } from "./client"
|
||||
|
||||
const ADMIN_SELF_REGISTRATION_KEY = "admin.self_registration_enabled"
|
||||
|
||||
function resolveEnvFallback(): boolean {
|
||||
return process.env.CMS_ADMIN_SELF_REGISTRATION_ENABLED === "true"
|
||||
}
|
||||
|
||||
function parseStoredBoolean(value: string): boolean | null {
|
||||
if (value === "true") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === "false") {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function isAdminSelfRegistrationEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const setting = await db.systemSetting.findUnique({
|
||||
where: { key: ADMIN_SELF_REGISTRATION_KEY },
|
||||
select: { value: true },
|
||||
})
|
||||
|
||||
if (!setting) {
|
||||
return resolveEnvFallback()
|
||||
}
|
||||
|
||||
const parsed = parseStoredBoolean(setting.value)
|
||||
|
||||
if (parsed === null) {
|
||||
return resolveEnvFallback()
|
||||
}
|
||||
|
||||
return parsed
|
||||
} catch {
|
||||
// Fallback while migrations are not yet applied in a local environment.
|
||||
return resolveEnvFallback()
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAdminSelfRegistrationEnabled(enabled: boolean): Promise<void> {
|
||||
await db.systemSetting.upsert({
|
||||
where: { key: ADMIN_SELF_REGISTRATION_KEY },
|
||||
create: {
|
||||
key: ADMIN_SELF_REGISTRATION_KEY,
|
||||
value: enabled ? "true" : "false",
|
||||
},
|
||||
update: {
|
||||
value: enabled ? "true" : "false",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-neutral-900 text-neutral-50 hover:bg-neutral-800",
|
||||
default: "bg-neutral-900 text-white hover:bg-neutral-800",
|
||||
secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200",
|
||||
ghost: "hover:bg-neutral-100 hover:text-neutral-900",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user