From f6331e722985aeac4723f6780b0a7f092f38ed89 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 22 May 2026 17:04:51 +0300 Subject: [PATCH 1/3] feat(auth): auto-hide OAuth buttons when secrets absent; add ALLOW_REGISTRATION toggle OAuth provider buttons (GitHub, Google) are now derived from server-side env vars at runtime via /api/auth/config, removing the error-prone manual NEXT_PUBLIC_SHOW_* flags. Setting ALLOW_REGISTRATION=false blocks sign-up at both the API layer (Better-Auth /sign-up/email and custom /register) and hides the register toggle in the UI. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 5 +-- web/.env.example | 17 +++++---- web/src/app/api/auth/[...all]/route.ts | 13 ++++++- web/src/app/api/auth/config/route.ts | 9 +++++ web/src/app/api/auth/register/route.ts | 4 +++ web/src/app/login/page.tsx | 50 ++++++++++++++++---------- 6 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 web/src/app/api/auth/config/route.ts diff --git a/.env.example b/.env.example index 3d5dae7..b7fa474 100644 --- a/.env.example +++ b/.env.example @@ -116,10 +116,11 @@ CACHE_DIR=/app/data/cache BETTER_AUTH_URL=http://localhost:3000 # Public URL of the web app (used for cookie security + trusted origins) FRONTEND_URL= # Set if different from BETTER_AUTH_URL (e.g. custom domain) AUTH_SECRET=change-me-in-development -GITHUB_CLIENT_ID= +GITHUB_CLIENT_ID= # Leave blank to hide "Sign in with GitHub" GITHUB_CLIENT_SECRET= -GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_ID= # Leave blank to hide "Sign in with Google" GOOGLE_CLIENT_SECRET= +ALLOW_REGISTRATION=true # Set to false to disable self-service account creation # RS256 JWT keys (cross-service auth: app → backend) # Generate with: diff --git a/web/.env.example b/web/.env.example index 4d6a24f..e0135dd 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,17 +1,16 @@ # NextAuth.js AUTH_SECRET=change-me-in-development # Generate for production with: openssl rand -base64 32 -# GitHub OAuth App -GITHUB_CLIENT_ID=your-github-client-id -GITHUB_CLIENT_SECRET=your-github-client-secret +# GitHub OAuth App — leave blank to hide the "Sign in with GitHub" button +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= -# Google OAuth App -GOOGLE_CLIENT_ID=your-google-client-id -GOOGLE_CLIENT_SECRET=your-google-client-secret +# Google OAuth App — leave blank to hide the "Sign in with Google" button +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= -# Login page — show OAuth buttons (omit to show credentials-only) -NEXT_PUBLIC_SHOW_GITHUB=true -NEXT_PUBLIC_SHOW_GOOGLE=true +# Set to false to disable self-service account creation (UI toggle and API both blocked) +ALLOW_REGISTRATION=true # Atlassian OAuth 2.0 + PKCE — client-side only, no secret required. # Register an OAuth 2.0 (3LO) app at https://developer.atlassian.com/console/myapps/ diff --git a/web/src/app/api/auth/[...all]/route.ts b/web/src/app/api/auth/[...all]/route.ts index 9900a3b..3881e3a 100644 --- a/web/src/app/api/auth/[...all]/route.ts +++ b/web/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,15 @@ import { auth } from '@/lib/auth'; import { toNextJsHandler } from 'better-auth/next-js'; +import { NextRequest, NextResponse } from 'next/server'; -export const { GET, POST } = toNextJsHandler(auth); +const handlers = toNextJsHandler(auth); + +export const GET = handlers.GET; + +export async function POST(request: NextRequest) { + const url = new URL(request.url); + if (process.env.ALLOW_REGISTRATION === 'false' && url.pathname.endsWith('/sign-up/email')) { + return NextResponse.json({ error: 'Registration is disabled' }, { status: 403 }); + } + return handlers.POST(request); +} diff --git a/web/src/app/api/auth/config/route.ts b/web/src/app/api/auth/config/route.ts new file mode 100644 index 0000000..ec9574a --- /dev/null +++ b/web/src/app/api/auth/config/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ + github: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET), + google: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET), + registrationEnabled: process.env.ALLOW_REGISTRATION !== 'false', + }); +} diff --git a/web/src/app/api/auth/register/route.ts b/web/src/app/api/auth/register/route.ts index 2f4981c..19da03d 100644 --- a/web/src/app/api/auth/register/route.ts +++ b/web/src/app/api/auth/register/route.ts @@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'; import { auth } from '@/lib/auth'; export async function POST(request: Request) { + if (process.env.ALLOW_REGISTRATION === 'false') { + return NextResponse.json({ error: 'Registration is disabled' }, { status: 403 }); + } + let body: { username?: string; password?: string; email?: string }; try { body = await request.json(); diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 30252d2..957ee4a 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useSearchParams } from 'next/navigation'; -import { FormEvent, Suspense, useCallback, useState } from 'react'; +import { FormEvent, Suspense, useCallback, useEffect, useState } from 'react'; import { Box, Button, @@ -17,9 +17,6 @@ import GoogleIcon from '@mui/icons-material/Google'; import { createAppTheme } from '../../spa/theme'; import { useThemeMode } from '../../spa/hooks/useThemeMode'; -const showGithub = process.env.NEXT_PUBLIC_SHOW_GITHUB !== 'false'; -const showGoogle = process.env.NEXT_PUBLIC_SHOW_GOOGLE !== 'false'; - function LoginForm() { const searchParams = useSearchParams(); const error = searchParams.get('error'); @@ -31,6 +28,11 @@ function LoginForm() { ? rawCallback : '/'; + const [authConfig, setAuthConfig] = useState<{ + github: boolean; + google: boolean; + registrationEnabled: boolean; + } | null>(null); const [mode, setMode] = useState<'login' | 'register'>('login'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -38,6 +40,16 @@ function LoginForm() { const [formError, setFormError] = useState(null); const [loading, setLoading] = useState(false); + useEffect(() => { + fetch('/api/auth/config') + .then((r) => r.json()) + .then((cfg) => { + setAuthConfig(cfg); + if (!cfg.registrationEnabled) setMode('login'); + }) + .catch(() => setAuthConfig({ github: false, google: false, registrationEnabled: true })); + }, []); + const handleCredentialsLogin = useCallback( async (e: FormEvent) => { e.preventDefault(); @@ -198,19 +210,21 @@ function LoginForm() { - + {authConfig?.registrationEnabled && ( + + )} - {(showGithub || showGoogle) && ( + {(authConfig?.github || authConfig?.google) && ( <> @@ -218,7 +232,7 @@ function LoginForm() { - {showGithub && ( + {authConfig.github && ( - {authConfig?.registrationEnabled && ( + {authConfig.registrationEnabled && (