diff --git a/.env.example b/.env.example index c74f19c..0e3f822 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,14 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000 # Rotating this key invalidates all existing ciphertexts; re-enter each secret. SECRETS_ENCRYPTION_KEY= +# OAuth provider buttons on the login page (optional). +# Set to "true" only for a provider you have actually configured in Supabase +# Auth. Unset/anything-else hides that button (an unconfigured provider button +# just dead-ends in a Supabase error). If neither is "true" the whole +# "OR / social sign-in" block is hidden. +NEXT_PUBLIC_OAUTH_GITHUB= +NEXT_PUBLIC_OAUTH_GOOGLE= + # Node env (optional, defaults to "development" locally) NODE_ENV=development diff --git a/__tests__/auth-config.test.ts b/__tests__/auth-config.test.ts new file mode 100644 index 0000000..56db36d --- /dev/null +++ b/__tests__/auth-config.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { resolveOAuthProviders, anyOAuthProvider } from '@/lib/auth-config' + +describe('resolveOAuthProviders', () => { + it('enables a provider only when its flag is exactly "true"', () => { + expect(resolveOAuthProviders({ github: 'true', google: 'true' })).toEqual({ + github: true, + google: true, + }) + }) + + it('treats unset / empty / other values as disabled', () => { + expect(resolveOAuthProviders({})).toEqual({ github: false, google: false }) + expect(resolveOAuthProviders({ github: '', google: 'false' })).toEqual({ + github: false, + google: false, + }) + expect(resolveOAuthProviders({ github: 'TRUE', google: '1' })).toEqual({ + github: false, + google: false, + }) + }) + + it('resolves each provider independently', () => { + expect(resolveOAuthProviders({ github: 'true' })).toEqual({ + github: true, + google: false, + }) + }) +}) + +describe('anyOAuthProvider', () => { + it('is false when no provider is enabled', () => { + expect(anyOAuthProvider({ github: false, google: false })).toBe(false) + }) + + it('is true when at least one provider is enabled', () => { + expect(anyOAuthProvider({ github: true, google: false })).toBe(true) + expect(anyOAuthProvider({ github: false, google: true })).toBe(true) + }) +}) diff --git a/app/login/page.tsx b/app/login/page.tsx index 59b4d43..c6f7b2f 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -4,9 +4,12 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' +import { oauthProviders, anyOAuthProvider } from '@/lib/auth-config' export default function LoginPage() { const router = useRouter() + const oauth = oauthProviders() + const showOAuth = anyOAuthProvider(oauth) const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') @@ -125,39 +128,47 @@ export default function LoginPage() { -
-
- OR -
-
+ {showOAuth && ( + <> +
+
+ OR +
+
-
- - -
+
+ {oauth.github && ( + + )} + {oauth.google && ( + + )} +
+ + )}

no account?{' '} diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b008996..c8f9260 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -24,6 +24,10 @@ All variables are listed in [.env.example](../.env.example). At minimum: - `SUPABASE_SERVICE_ROLE_KEY` — service-role JWT, server-side only - `NEXT_PUBLIC_APP_URL` — public URL of the deployed app, used in OAuth redirects +Optional: + +- `NEXT_PUBLIC_OAUTH_GITHUB` / `NEXT_PUBLIC_OAUTH_GOOGLE` — set to `"true"` only for an OAuth provider you have actually configured in Supabase Auth. The login page hides any provider button that is not enabled, and hides the whole social-sign-in block if neither is set. These are build-time public values, so a change requires a redeploy to take effect. + ## How a deploy runs 1. A developer pushes to `main`. diff --git a/lib/auth-config.ts b/lib/auth-config.ts new file mode 100644 index 0000000..1ab029e --- /dev/null +++ b/lib/auth-config.ts @@ -0,0 +1,33 @@ +// Which third-party OAuth providers this deployment has wired up in Supabase. +// A provider button that points at an unconfigured provider just dead-ends in +// a Supabase error, so the buttons are opt-in: shown only when the deployer +// explicitly sets the flag. Flags are NEXT_PUBLIC_ because the login page is a +// client component and needs the value in the browser bundle. + +export type OAuthProvider = 'github' | 'google' + +// Pure core: decide enabled providers from the raw flag strings. Unit-tested. +export function resolveOAuthProviders(flags: { + github?: string + google?: string +}): Record { + return { + github: flags.github === 'true', + google: flags.google === 'true', + } +} + +// Env-reading wrapper. The process.env member expressions are written as +// literals so Next.js statically inlines them into the client bundle. +export function oauthProviders(): Record { + return resolveOAuthProviders({ + github: process.env.NEXT_PUBLIC_OAUTH_GITHUB, + google: process.env.NEXT_PUBLIC_OAUTH_GOOGLE, + }) +} + +export function anyOAuthProvider( + providers: Record, +): boolean { + return providers.github || providers.google +}