| [834bcc3] | 1 | // Google OAuth2 for LISTENERS (commenting). Per-instance: each self-hoster sets
|
|---|
| 2 | // their OWN Google client. This way every site is tied to its own Google Cloud
|
|---|
| 3 | // project — no central dependency, no shared liability.
|
|---|
| [32cc601] | 4 | //
|
|---|
| [834bcc3] | 5 | // Config source (in this order): app_settings (set via Admin → Settings),
|
|---|
| 6 | // otherwise env vars. Not configured → no "Login with Google" button; the rest
|
|---|
| 7 | // of the site keeps working. Google login NEVER grants admin rights.
|
|---|
| [32cc601] | 8 | //
|
|---|
| [834bcc3] | 9 | // The redirect URI is derived from PUBLIC_BASE_URL (<base>/auth/google/callback),
|
|---|
| 10 | // or explicitly via GOOGLE_REDIRECT_URI. That exact URL must be listed in Google Cloud.
|
|---|
| [32cc601] | 11 |
|
|---|
| [2248cd2] | 12 | import { getSetting } from '../services/SettingsService.js';
|
|---|
| [32cc601] | 13 |
|
|---|
| [9e27d64] | 14 | const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|---|
| 15 | const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|---|
| 16 | const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
|
|---|
| [32cc601] | 17 |
|
|---|
| [834bcc3] | 18 | // Read dynamically (UI changes take effect without a restart). app_settings wins, env = fallback.
|
|---|
| [2248cd2] | 19 | function clientId() {
|
|---|
| 20 | return getSetting('google_client_id', '') || process.env.GOOGLE_CLIENT_ID || '';
|
|---|
| 21 | }
|
|---|
| 22 | function clientSecret() {
|
|---|
| 23 | return getSetting('google_client_secret', '') || process.env.GOOGLE_CLIENT_SECRET || '';
|
|---|
| 24 | }
|
|---|
| 25 | export function redirectUri() {
|
|---|
| 26 | if (process.env.GOOGLE_REDIRECT_URI) return process.env.GOOGLE_REDIRECT_URI;
|
|---|
| 27 | const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
|
|---|
| 28 | return base ? `${base}/auth/google/callback` : '';
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| [834bcc3] | 31 | export function currentClientId() { return clientId(); } // not secret, used for the settings form
|
|---|
| [2248cd2] | 32 | export function clientSecretSet() { return !!clientSecret(); }
|
|---|
| [9e27d64] | 33 | export function googleConfigured() {
|
|---|
| [2248cd2] | 34 | return !!(clientId() && clientSecret() && redirectUri());
|
|---|
| [c80e78b] | 35 | }
|
|---|
| 36 |
|
|---|
| [9e27d64] | 37 | export function authorizeUrl(state) {
|
|---|
| 38 | const p = new URLSearchParams({
|
|---|
| [2248cd2] | 39 | client_id: clientId(),
|
|---|
| 40 | redirect_uri: redirectUri(),
|
|---|
| [9e27d64] | 41 | response_type: 'code',
|
|---|
| 42 | scope: 'openid email profile',
|
|---|
| 43 | state,
|
|---|
| 44 | access_type: 'online',
|
|---|
| 45 | prompt: 'select_account',
|
|---|
| 46 | });
|
|---|
| 47 | return `${AUTH_URL}?${p.toString()}`;
|
|---|
| [32cc601] | 48 | }
|
|---|
| 49 |
|
|---|
| [9e27d64] | 50 | export async function exchangeCode(code) {
|
|---|
| 51 | const body = new URLSearchParams({
|
|---|
| 52 | code,
|
|---|
| [2248cd2] | 53 | client_id: clientId(),
|
|---|
| 54 | client_secret: clientSecret(),
|
|---|
| 55 | redirect_uri: redirectUri(),
|
|---|
| [9e27d64] | 56 | grant_type: 'authorization_code',
|
|---|
| 57 | });
|
|---|
| 58 | const r = await fetch(TOKEN_URL, {
|
|---|
| 59 | method: 'POST',
|
|---|
| 60 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|---|
| 61 | body,
|
|---|
| [c80e78b] | 62 | });
|
|---|
| [834bcc3] | 63 | if (!r.ok) throw new Error(`Google token exchange failed: ${r.status}`);
|
|---|
| [9e27d64] | 64 | return r.json(); // { access_token, id_token, ... }
|
|---|
| [c80e78b] | 65 | }
|
|---|
| 66 |
|
|---|
| [9e27d64] | 67 | // Returns { sub, email, email_verified, name, picture }.
|
|---|
| 68 | export async function fetchUserinfo(accessToken) {
|
|---|
| 69 | const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|---|
| [834bcc3] | 70 | if (!r.ok) throw new Error(`Google userinfo failed: ${r.status}`);
|
|---|
| [9e27d64] | 71 | return r.json();
|
|---|
| [c80e78b] | 72 | }
|
|---|