Skip to content

Commit 0e0a46c

Browse files
committed
feat: add NEXT_PUBLIC_SIGNUP_DISABLED flag to disable user registration
When NEXT_PUBLIC_SIGNUP_DISABLED=true, the following changes take effect: - /api/auth/signup returns 403 SIGNUP_DISABLED error - Signup page redirects to home - All signup links hidden across the app (landing page, header, sidebar, mobile sidebar, sign-in page, discover template detail, privacy/terms pages) - Existing users can still sign in Set via docker-compose environment or .env.local to enable.
1 parent c45b66d commit 0e0a46c

13 files changed

Lines changed: 136 additions & 59 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,11 @@ CLOUDFLARE_TUNNEL_TOKEN=
8989
# GitHub OAuth
9090
# GITHUB_CLIENT_ID=
9191
# GITHUB_CLIENT_SECRET=
92+
93+
# =============================================================================
94+
# Optional: Disable Signup
95+
# =============================================================================
96+
97+
# Set to "true" to disable new user registration.
98+
# Existing users can still sign in. Signup links and the signup page will be hidden.
99+
# NEXT_PUBLIC_SIGNUP_DISABLED=true

docker-compose.dev.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ services:
1818
- NEXT_PUBLIC_POCKETBASE_URL=http://localhost:8090
1919
# Server-side URL (container-to-container)
2020
- POCKETBASE_URL=http://pocketbase:8090
21+
# Disable signup when set to "true"
22+
- NEXT_PUBLIC_SIGNUP_DISABLED=${NEXT_PUBLIC_SIGNUP_DISABLED:-false}
2123
depends_on:
2224
pocketbase:
2325
condition: service_healthy

src/app/(auth)/signin/page.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,19 @@ function SignInForm() {
257257
</Button>
258258
</div>
259259

260-
<div className="mt-6 text-center">
261-
<p className="text-sm text-muted-foreground">
262-
Don&apos;t have an account?{" "}
263-
<Link
264-
href={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : "/signup"}
265-
className="text-primary font-medium hover:underline"
266-
>
267-
Sign up
268-
</Link>
269-
</p>
270-
</div>
260+
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "true" && (
261+
<div className="mt-6 text-center">
262+
<p className="text-sm text-muted-foreground">
263+
Don&apos;t have an account?{" "}
264+
<Link
265+
href={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : "/signup"}
266+
className="text-primary font-medium hover:underline"
267+
>
268+
Sign up
269+
</Link>
270+
</p>
271+
</div>
272+
)}
271273
</>
272274
);
273275
}

src/app/(auth)/signup/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, Suspense } from "react";
3+
import { useState, Suspense, useEffect } from "react";
44
import Link from "next/link";
55
import { useRouter, useSearchParams } from "next/navigation";
66
import { motion } from "framer-motion";
@@ -71,6 +71,12 @@ function SignUpForm() {
7171
const searchParams = useSearchParams();
7272
const returnTo = searchParams.get("returnTo");
7373
const validReturnUrl = getValidReturnUrl(returnTo);
74+
75+
useEffect(() => {
76+
if (process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "true") {
77+
router.replace("/");
78+
}
79+
}, [router]);
7480

7581
const [isLoading, setIsLoading] = useState(false);
7682
const [errors, setErrors] = useState<FormErrors>({});

src/app/(dashboard)/discover/[id]/template-detail-client.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,11 +1088,13 @@ export function TemplateDetailClient({
10881088
Sign In
10891089
</Link>
10901090
</Button>
1091-
<Button asChild className="rounded-xl">
1092-
<Link href={`/signup?returnTo=/discover/${template.id}`}>
1093-
Sign Up
1094-
</Link>
1095-
</Button>
1091+
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "true" && (
1092+
<Button asChild className="rounded-xl">
1093+
<Link href={`/signup?returnTo=/discover/${template.id}`}>
1094+
Sign Up
1095+
</Link>
1096+
</Button>
1097+
)}
10961098
</div>
10971099
</div>
10981100
</CardContent>

src/app/api/auth/demo-reset/route.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { createAdminClient } from '@/lib/pocketbase';
3+
import { getDefaultPreferences } from '@/lib/services/preferences';
34

45
const DEMO_EMAIL = process.env.DEMO_USER_EMAIL || 'demo@checkmate.local';
56

67
const DEMO_OWNED_COLLECTIONS = [
7-
'instances',
8+
'activityLog',
9+
'notifications',
10+
'collaborators',
811
'instanceItems',
9-
'blueprints',
12+
'instances',
1013
'items',
14+
'blueprints',
1115
'workspaces',
12-
'collaborators',
13-
'notifications',
14-
'activityLog',
1516
] as const;
1617

1718
const COLLECTION_OWNER_FIELDS: Record<string, string> = {
@@ -28,9 +29,7 @@ const COLLECTION_OWNER_FIELDS: Record<string, string> = {
2829
export async function resetDemoUserData(userId: string): Promise<void> {
2930
const adminPb = await createAdminClient();
3031

31-
const deleteOrder = [...DEMO_OWNED_COLLECTIONS].reverse();
32-
33-
for (const collection of deleteOrder) {
32+
for (const collection of DEMO_OWNED_COLLECTIONS) {
3433
try {
3534
const ownerField = COLLECTION_OWNER_FIELDS[collection] || 'createdBy';
3635
const records = await adminPb.collection(collection).getFullList({
@@ -49,6 +48,11 @@ export async function resetDemoUserData(userId: string): Promise<void> {
4948
}
5049
}
5150

51+
await adminPb.collection('users').update(userId, {
52+
displayName: 'Demo User',
53+
preferences: getDefaultPreferences(),
54+
});
55+
5256
console.log(`[Demo Reset] Wiped data for demo user ${userId}`);
5357
}
5458

src/app/api/auth/signup/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ interface SignUpResponse {
4444
// ============================================================================
4545

4646
export async function POST(request: NextRequest): Promise<NextResponse<SignUpResponse>> {
47+
if (process.env.NEXT_PUBLIC_SIGNUP_DISABLED === 'true') {
48+
return NextResponse.json(
49+
{
50+
success: false,
51+
error: {
52+
code: 'SIGNUP_DISABLED',
53+
message: 'New account registration is currently disabled.',
54+
timestamp: new Date().toISOString(),
55+
},
56+
},
57+
{ status: 403 }
58+
);
59+
}
60+
4761
try {
4862
// Parse request body
4963
const body = await request.json() as SignUpRequestBody;

src/app/page.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ThemeToggle } from "@/components/ui/theme-toggle";
66
import { Logo } from "@/components/ui/logo";
77

88
export default function Home() {
9+
const signupDisabled = process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "true";
10+
911
return (
1012
<div className="flex min-h-screen flex-col">
1113
{/* Header */}
@@ -21,9 +23,11 @@ export default function Home() {
2123
<Button variant="ghost" asChild>
2224
<Link href="/signin">Sign in</Link>
2325
</Button>
24-
<Button asChild size="sm" className="h-10 px-4 text-base">
25-
<Link href="/signup">Get Started</Link>
26-
</Button>
26+
{!signupDisabled && (
27+
<Button asChild size="sm" className="h-10 px-4 text-base">
28+
<Link href="/signup">Get Started</Link>
29+
</Button>
30+
)}
2731
</div>
2832

2933
{/* Mobile Nav */}
@@ -47,9 +51,11 @@ export default function Home() {
4751
<Button variant="ghost" asChild className="justify-start">
4852
<Link href="/signin">Sign in</Link>
4953
</Button>
50-
<Button asChild className="justify-start">
51-
<Link href="/signup">Get Started</Link>
52-
</Button>
54+
{!signupDisabled && (
55+
<Button asChild className="justify-start">
56+
<Link href="/signup">Get Started</Link>
57+
</Button>
58+
)}
5359
</div>
5460
</SheetContent>
5561
</Sheet>
@@ -72,12 +78,14 @@ export default function Home() {
7278
</p>
7379
</div>
7480
<div className="flex flex-col gap-4 sm:flex-row">
75-
<Button size="lg" asChild className="h-12 px-8 text-base">
76-
<Link href="/signup">
77-
Get Started Free
78-
<ArrowRight className="ml-2 h-4 w-4" />
79-
</Link>
80-
</Button>
81+
{!signupDisabled && (
82+
<Button size="lg" asChild className="h-12 px-8 text-base">
83+
<Link href="/signup">
84+
Get Started Free
85+
<ArrowRight className="ml-2 h-4 w-4" />
86+
</Link>
87+
</Button>
88+
)}
8189
<Button size="lg" variant="outline" asChild className="h-12 px-8 text-base border-primary/20 hover:bg-primary/5 hover:border-primary/50 transition-colors">
8290
<Link href="/discover">Browse Public Checklists</Link>
8391
</Button>

src/app/privacy/page.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22
import { ListChecks } from "lucide-react";
33

44
export default function PrivacyPage() {
5+
const signupDisabled = process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "true";
56
return (
67
<div className="min-h-screen bg-background">
78
{/* Header */}
@@ -20,12 +21,14 @@ export default function PrivacyPage() {
2021
>
2122
Sign in
2223
</Link>
23-
<Link
24-
href="/signup"
25-
className="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
26-
>
27-
Get Started
28-
</Link>
24+
{!signupDisabled && (
25+
<Link
26+
href="/signup"
27+
className="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
28+
>
29+
Get Started
30+
</Link>
31+
)}
2932
</nav>
3033
</div>
3134
</header>

src/app/terms/page.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22
import { ListChecks } from "lucide-react";
33

44
export default function TermsPage() {
5+
const signupDisabled = process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "true";
56
return (
67
<div className="min-h-screen bg-background">
78
{/* Header */}
@@ -20,12 +21,14 @@ export default function TermsPage() {
2021
>
2122
Sign in
2223
</Link>
23-
<Link
24-
href="/signup"
25-
className="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
26-
>
27-
Get Started
28-
</Link>
24+
{!signupDisabled && (
25+
<Link
26+
href="/signup"
27+
className="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
28+
>
29+
Get Started
30+
</Link>
31+
)}
2932
</nav>
3033
</div>
3134
</header>

0 commit comments

Comments
 (0)