From 522540452cd1a96e1e197a4240974fdd0ecb65c2 Mon Sep 17 00:00:00 2001 From: Richard R Date: Thu, 14 May 2026 12:47:05 -0600 Subject: [PATCH] feat(auth): add runtime toggle for user sign-ups and enforce signup policy Introduce `enableUserSignups` runtime setting to allow administrators to control whether new accounts can be created. Update environment variable and documentation references to support this feature. UI elements for account creation are now conditionally rendered based on this flag. Signup attempts are blocked server-side when disabled, including email, OAuth, and anonymous upgrades. Add `assertUserSignupAllowed` utility for consistent enforcement and corresponding unit tests to verify policy behavior. --- .env.example | 3 +- docs-site/docs/configure/admin-panel.md | 1 + docs-site/docs/configure/auth.md | 2 +- docs-site/docs/deploy/vercel-deployment.md | 1 + .../docs/reference/environment-variables.md | 10 ++++++ src/app/(app)/signin/page.tsx | 16 +++++---- src/app/(app)/signup/page.tsx | 27 +++++++++++++++ src/app/(public)/layout.tsx | 8 +++-- src/app/(public)/page.tsx | 10 ++++-- src/components/SettingsModal.tsx | 22 ++++++++----- src/components/admin/AdminFeaturesPanel.tsx | 8 +++++ src/components/auth/RateLimitBanner.tsx | 4 ++- src/components/auth/UserMenu.tsx | 14 +++++--- src/contexts/RuntimeConfigContext.tsx | 2 ++ src/lib/server/admin/settings.ts | 1 + src/lib/server/auth/auth.ts | 7 ++++ src/lib/server/auth/signup-policy.ts | 12 +++++++ tests/unit/signup-policy.spec.ts | 33 +++++++++++++++++++ 18 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 src/lib/server/auth/signup-policy.ts create mode 100644 tests/unit/signup-policy.spec.ts diff --git a/.env.example b/.env.example index 77dc817..d390755 100644 --- a/.env.example +++ b/.env.example @@ -90,9 +90,10 @@ FFMPEG_BIN= # NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=true # NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=true # NEXT_PUBLIC_ENABLE_TTS_PROVIDERS_TAB=true +# NEXT_PUBLIC_ENABLE_USER_SIGNUPS=true # NEXT_PUBLIC_RESTRICT_USER_API_KEYS=true # NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=custom-openai # NEXT_PUBLIC_CHANGELOG_FEED_URL=https://docs.openreader.richardr.dev/changelog/manifest.json # NEXT_PUBLIC_DEFAULT_TTS_MODEL=kokoro # NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true -# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false +# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=true diff --git a/docs-site/docs/configure/admin-panel.md b/docs-site/docs/configure/admin-panel.md index 2bd5d15..7167962 100644 --- a/docs-site/docs/configure/admin-panel.md +++ b/docs-site/docs/configure/admin-panel.md @@ -66,6 +66,7 @@ Runtime-editable settings, one row per key: | --- | --- | | `defaultTtsProvider` | Default provider id new users start with (built-in id or shared slug). | | `changelogFeedUrl` | Public changelog manifest URL used by the Settings modal changelog panel. | +| `enableUserSignups` | Controls whether new accounts can be created. Existing accounts can still sign in when this is `false`. | | `restrictUserApiKeys` | Restrict user-supplied API keys/base URLs; when `true`, only admin shared providers are allowed. | | `enableTtsProvidersTab` | Whether the user-facing TTS Provider tab in Settings is shown. | | `showAllProviderModels` | When `false`, users are restricted to each provider's default model (shared provider `defaultModel` or built-in provider default). | diff --git a/docs-site/docs/configure/auth.md b/docs-site/docs/configure/auth.md index fcedf3f..630a29e 100644 --- a/docs-site/docs/configure/auth.md +++ b/docs-site/docs/configure/auth.md @@ -31,7 +31,7 @@ ADMIN_EMAILS=alice@example.com,bob@example.com Admins see a new **Admin** tab in **Settings** with two sub-tabs: - **Shared TTS providers** — server-managed TTS provider instances with encrypted keys, visible to all users. -- **Site features** — runtime overrides for what were previously `NEXT_PUBLIC_*` build-time flags (default TTS provider/model, word highlighting, audiobook export, etc.). +- **Site features** — runtime overrides for what were previously `NEXT_PUBLIC_*` build-time flags (including account signup availability, default TTS provider/model, word highlighting, audiobook export, etc.). Admin assignment is reconciled on every session resolution, so removing an email from `ADMIN_EMAILS` demotes the user on next login without a restart. See [Admin Panel](./admin-panel) for the full reference. diff --git a/docs-site/docs/deploy/vercel-deployment.md b/docs-site/docs/deploy/vercel-deployment.md index a32c0e3..88adbee 100644 --- a/docs-site/docs/deploy/vercel-deployment.md +++ b/docs-site/docs/deploy/vercel-deployment.md @@ -53,6 +53,7 @@ After the first successful deploy and admin login, open **Settings → Admin** a - `enableDocxConversion=false` on Vercel (`soffice` unavailable). - `enableDestructiveDeleteActions=false` for safer public deployments. - `enableTtsProvidersTab=false` if you want shared-provider-only UX. + - `enableUserSignups=true` unless you explicitly want an invite-only deployment. - `restrictUserApiKeys=true` to block user BYOK through the hosted server. - `defaultTtsProvider=replicate` (or your preferred shared slug). - `showAllProviderModels=false` if you want users locked to each provider's default model. diff --git a/docs-site/docs/reference/environment-variables.md b/docs-site/docs/reference/environment-variables.md index d2414de..ef79788 100644 --- a/docs-site/docs/reference/environment-variables.md +++ b/docs-site/docs/reference/environment-variables.md @@ -18,6 +18,7 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o | `API_KEY` | Legacy bootstrap seed | none | Optional first-boot seed into `default-openai`; then manage in Settings → Admin → Shared providers | | `NEXT_PUBLIC_*` runtime seeds | Legacy bootstrap seed | varies | Optional first-boot seeds for site features; then manage in Settings → Admin → Site features | | `NEXT_PUBLIC_CHANGELOG_FEED_URL` | Legacy bootstrap seed | `https://docs.openreader.richardr.dev/changelog/manifest.json` | Optional first-boot seed for changelog feed URL; then manage in Settings → Admin → Site features | +| `NEXT_PUBLIC_ENABLE_USER_SIGNUPS` | Legacy bootstrap seed | `true` | Optional first-boot seed for whether new accounts can be created; then manage in Settings → Admin → Site features | | `TTS_CACHE_MAX_SIZE_BYTES` | TTS caching | `268435456` (256 MB) | Tune in-memory TTS cache size | | `TTS_CACHE_TTL_MS` | TTS caching | `1800000` (30 min) | Tune in-memory TTS cache TTL | | `TTS_MAX_RETRIES` | TTS retry | `2` | Tune retry attempts for upstream 429/5xx | @@ -387,6 +388,15 @@ Controls whether the **TTS Provider** section appears in the user-facing Setting - Set `false` to hide provider/model/API controls in the per-user Settings modal (the admin panel is unaffected). - Runtime key: `enableTtsProvidersTab` +### NEXT_PUBLIC_ENABLE_USER_SIGNUPS + +Controls whether new user accounts can be created. + +- Default: `true` (enabled) +- When `false`, new account creation is blocked for email sign-up, first-time OAuth signup, and anonymous-to-account upgrades. +- Existing users can still sign in. +- Runtime key: `enableUserSignups` + ### NEXT_PUBLIC_RESTRICT_USER_API_KEYS Controls whether users can supply personal API keys/base URLs for built-in providers. diff --git a/src/app/(app)/signin/page.tsx b/src/app/(app)/signin/page.tsx index 4a26650..01aff67 100644 --- a/src/app/(app)/signin/page.tsx +++ b/src/app/(app)/signin/page.tsx @@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { getAuthClient } from '@/lib/client/auth-client'; import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; +import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { showPrivacyModal } from '@/components/PrivacyModal'; import { GithubIcon } from '@/components/icons/Icons'; import { LoadingSpinner } from '@/components/Spinner'; @@ -31,6 +32,7 @@ function SignInContent() { const [sessionExpired, setSessionExpired] = useState(false); const [error, setError] = useState(null); const { authEnabled, baseUrl, allowAnonymousAuthSessions, githubAuthEnabled } = useAuthConfig(); + const enableUserSignups = useFeatureFlag('enableUserSignups'); const { refresh: refreshRateLimit } = useAuthRateLimit(); const isAnyLoading = loadingEmail || loadingGithub || loadingAnonymous; @@ -240,12 +242,14 @@ function SignInContent() { {/* Footer */}
-

- Don't have an account?{' '} - - Sign up - -

+ {enableUserSignups && ( +

+ Don't have an account?{' '} + + Sign up + +

+ )}

By signing in, you agree to our{' '}