diff --git a/TODO.md b/TODO.md index 97913f8..ee0fa67 100644 --- a/TODO.md +++ b/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 diff --git a/apps/admin/src/app/login/login-form.tsx b/apps/admin/src/app/login/login-form.tsx index df017ec..473d81a 100644 --- a/apps/admin/src/app/login/login-form.tsx +++ b/apps/admin/src/app/login/login-form.tsx @@ -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(null) const [success, setSuccess] = useState(null) + const canSubmitSignUp = mode === "signup-owner" || mode === "signup-user" async function handleSignIn(event: FormEvent) { 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")}

{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.", + )}

@@ -208,7 +216,7 @@ export function LoginForm({ mode }: LoginFormProps) { {error ?

{error}

: null} - ) : ( + ) : canSubmitSignUp ? (
{error}

: null} {success ?

{success}

: null}
+ ) : ( +
+

+ {t( + "auth.messages.registrationDisabled", + "Registration is disabled for this admin instance. Ask an administrator to create an account or enable self-registration.", + )} +

+

+ + {t("auth.links.goToSignIn", "Go to sign in")} + +

+
)} ) diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index b55c5a2..03e3216 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -200,6 +200,12 @@ export default async function AdminHomePage({ > {t("dashboard.actions.openRoadmap", "Open roadmap and progress")} + + {t("settings.title", "Settings")} + diff --git a/apps/admin/src/app/register/page.tsx b/apps/admin/src/app/register/page.tsx index 648587b..2790154 100644 --- a/apps/admin/src/app/register/page.tsx +++ b/apps/admin/src/app/register/page.tsx @@ -33,7 +33,7 @@ export default async function RegisterPage({ searchParams }: { searchParams: Sea const enabled = await isSelfRegistrationEnabled() if (!enabled) { - redirect(`/login?next=${encodeURIComponent(nextPath)}`) + return } return diff --git a/apps/admin/src/app/settings/page.tsx b/apps/admin/src/app/settings/page.tsx new file mode 100644 index 0000000..e1793d0 --- /dev/null +++ b/apps/admin/src/app/settings/page.tsx @@ -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> + +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 ( +
+
+
+

+ {t("settings.badge", "Admin Settings")} +

+ +
+

{t("settings.title", "Settings")}

+

+ {t( + "settings.description", + "Manage runtime policies for the admin authentication and onboarding flow.", + )} +

+
+ + {t("settings.actions.backToDashboard", "Back to dashboard")} + +
+
+ + {notice ? ( +
+ {notice} +
+ ) : null} + + {error ? ( +
+ {error} +
+ ) : null} + +
+
+
+

+ {t("settings.registration.title", "Admin self-registration")} +

+

+ {t( + "settings.registration.description", + "When enabled, /register can create additional admin accounts after initial owner bootstrap.", + )} +

+
+ +
+

+ {t("settings.registration.currentStatusLabel", "Current status")}:{" "} + + {isRegistrationEnabled + ? t("settings.registration.status.enabled", "Enabled") + : t("settings.registration.status.disabled", "Disabled")} + +

+
+ +
+ + + +
+
+
+
+ ) +} diff --git a/apps/admin/src/i18n/messages.test.ts b/apps/admin/src/i18n/messages.test.ts index 4b2e987..6f9852e 100644 --- a/apps/admin/src/i18n/messages.test.ts +++ b/apps/admin/src/i18n/messages.test.ts @@ -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", diff --git a/apps/admin/src/lib/access.ts b/apps/admin/src/lib/access.ts index 068ca14..dec492c 100644 --- a/apps/admin/src/lib/access.ts +++ b/apps/admin/src/lib/access.ts @@ -43,6 +43,13 @@ const guardRules: GuardRule[] = [ scope: "global", }, }, + { + route: /^\/settings(?:\/|$)/, + requirement: { + permission: "users:manage_roles", + scope: "global", + }, + }, { route: /^\/(?:$|\?)/, requirement: { diff --git a/apps/admin/src/lib/auth/server.ts b/apps/admin/src/lib/auth/server.ts index e12fb11..8707f79 100644 --- a/apps/admin/src/lib/auth/server.ts +++ b/apps/admin/src/lib/auth/server.ts @@ -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 { } export async function isSelfRegistrationEnabled(): Promise { - // 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 { diff --git a/apps/admin/src/messages/de.json b/apps/admin/src/messages/de.json index 267ff3c..7554210 100644 --- a/apps/admin/src/messages/de.json +++ b/apps/admin/src/messages/de.json @@ -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", diff --git a/apps/admin/src/messages/en.json b/apps/admin/src/messages/en.json index b23adfb..6507ce4 100644 --- a/apps/admin/src/messages/en.json +++ b/apps/admin/src/messages/en.json @@ -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", diff --git a/apps/admin/src/messages/es.json b/apps/admin/src/messages/es.json index 52764f3..698e634 100644 --- a/apps/admin/src/messages/es.json +++ b/apps/admin/src/messages/es.json @@ -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", diff --git a/apps/admin/src/messages/fr.json b/apps/admin/src/messages/fr.json index 85acb74..50daaf6 100644 --- a/apps/admin/src/messages/fr.json +++ b/apps/admin/src/messages/fr.json @@ -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", diff --git a/docs/product-engineering/auth-baseline.md b/docs/product-engineering/auth-baseline.md index d604b2b..7a01497 100644 --- a/docs/product-engineering/auth-baseline.md +++ b/docs/product-engineering/auth-baseline.md @@ -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. diff --git a/packages/db/prisma/migrations/20260210200148_admin_registration_policy_setting/migration.sql b/packages/db/prisma/migrations/20260210200148_admin_registration_policy_setting/migration.sql new file mode 100644 index 0000000..4dbabbc --- /dev/null +++ b/packages/db/prisma/migrations/20260210200148_admin_registration_policy_setting/migration.sql @@ -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") +); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 33ce8dd..2154806 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -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") +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 91418cb..4f14908 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -7,3 +7,4 @@ export { registerPostCrudAuditHook, updatePost, } from "./posts" +export { isAdminSelfRegistrationEnabled, setAdminSelfRegistrationEnabled } from "./settings" diff --git a/packages/db/src/settings.ts b/packages/db/src/settings.ts new file mode 100644 index 0000000..fb3cd12 --- /dev/null +++ b/packages/db/src/settings.ts @@ -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 { + 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 { + 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", + }, + }) +} diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index be5462f..ab10d0e 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -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", },