diff --git a/.env.example b/.env.example index 0d1fd65..d51cf88 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ MONOCHROME_DEV_PORT=5173 AUTH_ENABLED=false AUTH_SECRET=change-me-to-a-random-string FIREBASE_PROJECT_ID=monochrome-database +# Optional: toggle login providers (defaults to true when unset) +# AUTH_GOOGLE_ENABLED=true +# AUTH_EMAIL_ENABLED=true # Optional: override the Firebase config for the login page (JSON string) # FIREBASE_CONFIG={"apiKey":"...","authDomain":"...","projectId":"...","storageBucket":"...","messagingSenderId":"...","appId":"..."} # Optional: set PocketBase URL (hides the field in settings when set) diff --git a/AUTH_GATE.md b/AUTH_GATE.md index 51464f8..7c02eb9 100644 --- a/AUTH_GATE.md +++ b/AUTH_GATE.md @@ -27,6 +27,8 @@ This document explains the optional server-side login gate and what it implies f - `AUTH_ENABLED=true` enables the gate (default is false). - `AUTH_SECRET` is required when the gate is enabled. It signs the session cookie. +- `AUTH_GOOGLE_ENABLED` toggles Google sign-in on `/login` (default true). +- `AUTH_EMAIL_ENABLED` toggles email/password sign-in on `/login` (default true). - `FIREBASE_PROJECT_ID` sets the Firebase project used to verify tokens. - `FIREBASE_CONFIG` (JSON) injects config into the login page. - `POCKETBASE_URL` hides the custom DB setting field. diff --git a/public/login.html b/public/login.html index dffa9f4..ecd40f7 100644 --- a/public/login.html +++ b/public/login.html @@ -277,24 +277,35 @@ const auth = getAuth(fbApp); const provider = new GoogleAuthProvider(); - // Reveal auth providers based on Firebase project config (non-blocking) - fetch(`https://www.googleapis.com/identitytoolkit/v3/relyingparty/getProjectConfig?key=${firebaseConfig.apiKey}`) - .then(res => res.ok ? res.json() : null) - .then(data => { - if (!data) return; - const providers = (data.idpConfig || []).filter(p => p.enabled).map(p => p.provider); - const hasGoogle = providers.includes('google.com'); - const hasPassword = (data.signIn || {}).allowPasswordSignup !== false; - if (hasGoogle) document.getElementById('google-btn').style.display = ''; - if (hasPassword) document.getElementById('email-form').style.display = ''; - if (hasGoogle && hasPassword) document.querySelector('.divider').style.display = ''; - }) - .catch(() => { - // Fallback: show everything if the check fails - document.getElementById('google-btn').style.display = ''; - document.getElementById('email-form').style.display = ''; - document.querySelector('.divider').style.display = ''; - }); + const googleBtn = document.getElementById('google-btn'); + const emailForm = document.getElementById('email-form'); + const divider = document.querySelector('.divider'); + const providerState = { + google: true, + password: true, + }; + const providerConfig = window.__AUTH_PROVIDERS__ || {}; + if (typeof providerConfig.google === 'boolean') providerState.google = providerConfig.google; + if (typeof providerConfig.password === 'boolean') providerState.password = providerConfig.password; + + const NO_PROVIDER_MESSAGE = 'No sign-in providers are enabled for this Firebase project.'; + + function renderProviders() { + if (googleBtn) googleBtn.style.display = providerState.google ? '' : 'none'; + if (emailForm) emailForm.style.display = providerState.password ? '' : 'none'; + if (divider) divider.style.display = providerState.google && providerState.password ? '' : 'none'; + if (!providerState.google && !providerState.password) { + showError(NO_PROVIDER_MESSAGE); + } + } + + function setProviderEnabled(providerName, enabled) { + if (providerName === 'google') providerState.google = enabled; + if (providerName === 'password') providerState.password = enabled; + renderProviders(); + } + + renderProviders(); function showError(msg) { const el = document.getElementById('error'); @@ -334,7 +345,10 @@ const result = await signInWithPopup(auth, provider); await sendTokenToServer(result.user); } catch (err) { - if (err.code !== 'auth/popup-closed-by-user') { + if (err.code === 'auth/operation-not-allowed') { + setProviderEnabled('google', false); + showError('Google sign-in is not enabled for this Firebase project.'); + } else if (err.code !== 'auth/popup-closed-by-user') { showError(err.message); } setLoading(false); @@ -353,7 +367,12 @@ const result = await signInWithEmailAndPassword(auth, email, password); await sendTokenToServer(result.user); } catch (err) { - showError(err.message); + if (err.code === 'auth/operation-not-allowed') { + setProviderEnabled('password', false); + showError('Email/password sign-in is not enabled for this Firebase project.'); + } else { + showError(err.message); + } setLoading(false); } }; diff --git a/vite-plugin-auth-gate.js b/vite-plugin-auth-gate.js index 910e6cc..b5a4c2f 100644 --- a/vite-plugin-auth-gate.js +++ b/vite-plugin-auth-gate.js @@ -33,6 +33,8 @@ export default function authGatePlugin() { const AUTH_ENABLED = (env.AUTH_ENABLED ?? 'false') !== 'false'; const FIREBASE_CONFIG = env.FIREBASE_CONFIG; const POCKETBASE_URL = env.POCKETBASE_URL; + const AUTH_GOOGLE_ENABLED = env.AUTH_GOOGLE_ENABLED; + const AUTH_EMAIL_ENABLED = env.AUTH_EMAIL_ENABLED; // Parse Firebase config once (used for injection + auth verification) let parsedFirebaseConfig = null; @@ -51,6 +53,16 @@ export default function authGatePlugin() { const flags = []; if (AUTH_ENABLED) flags.push('window.__AUTH_GATE__=true'); + const authProviderOverrides = {}; + if (AUTH_GOOGLE_ENABLED !== undefined) { + authProviderOverrides.google = AUTH_GOOGLE_ENABLED !== 'false'; + } + if (AUTH_EMAIL_ENABLED !== undefined) { + authProviderOverrides.password = AUTH_EMAIL_ENABLED !== 'false'; + } + if (Object.keys(authProviderOverrides).length > 0) { + flags.push(`window.__AUTH_PROVIDERS__=${JSON.stringify(authProviderOverrides)}`); + } if (parsedFirebaseConfig) flags.push(`window.__FIREBASE_CONFIG__=${JSON.stringify(parsedFirebaseConfig)}`); if (POCKETBASE_URL) flags.push(`window.__POCKETBASE_URL__=${JSON.stringify(POCKETBASE_URL)}`); const configScript = flags.length > 0 ? `` : null;