From dd646fb273c912979150a1f69705d94caab1ed2e Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Fri, 10 Apr 2026 02:06:44 +0000 Subject: [PATCH] feat: add Google/GitHub social login for Demo environment (GRO-531) - auth.ts: add google/github social providers from better-auth/social-providers - auth.ts: add getActiveProviders() to enumerate configured OAuth/social providers - index.ts: add /api/auth/providers public endpoint for frontend - App.tsx: update LoginPage to show Google/GitHub buttons based on /api/auth/providers response Co-Authored-By: Paperclip --- apps/api/src/index.ts | 7 ++- apps/api/src/lib/auth.ts | 36 +++++++++++- apps/web/src/App.tsx | 116 ++++++++++++++++++++++++++++++++------- 3 files changed, 138 insertions(+), 21 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e663986..1b146b9 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -2,7 +2,7 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; import { cors } from "hono/cors"; -import { getAuth, initAuth } from "./lib/auth.js"; +import { getAuth, initAuth, getActiveProviders } from "./lib/auth.js"; import { clientsRouter } from "./routes/clients.js"; import { petsRouter } from "./routes/pets.js"; import { servicesRouter } from "./routes/services.js"; @@ -92,6 +92,11 @@ app.get("/api/setup/status", async (c) => { return c.json({ needsSetup: !superUser }); }); +// Public auth providers endpoint — no auth required, tells frontend which login options are available +app.get("/api/auth/providers", async (c) => { + return c.json({ providers: getActiveProviders() }); +}); + // Protected API routes const api = app.basePath("/api"); api.use("*", authMiddleware); diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts index 0b0a872..020e540 100644 --- a/apps/api/src/lib/auth.ts +++ b/apps/api/src/lib/auth.ts @@ -1,6 +1,7 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { genericOAuth } from "better-auth/plugins"; +import { google, github } from "better-auth/social-providers"; import { getDb, authProviderConfig, eq } from "@groombook/db"; import { decryptSecret } from "@groombook/db"; @@ -27,6 +28,21 @@ export function getAuthPromise() { return authInitPromise; } +/** Returns which OAuth/social providers are configured via env vars. */ +export function getActiveProviders(): string[] { + const providers: string[] = []; + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + providers.push("google"); + } + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + providers.push("github"); + } + if (process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET) { + providers.push("authentik"); + } + return providers; +} + /** * Re-initializes the Better-Auth instance after auth config changes. * @@ -152,6 +168,23 @@ export async function initAuth(): Promise { console.log("[auth] Using env var config (no DB config found)"); } + const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); + const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); + + const socialPlugins = []; + if (hasGoogle) { + socialPlugins.push(google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + })); + } + if (hasGitHub) { + socialPlugins.push(github({ + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + })); + } + // Build Better-Auth instance using resolved config authInstance = betterAuth({ database: drizzleAdapter(db, { @@ -179,7 +212,8 @@ export async function initAuth(): Promise { }, ], }), - ], + ...socialPlugins, + ], session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9fc0d1b..efaefd3 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -22,12 +22,24 @@ import { useSession, signIn } from "./lib/auth-client.js"; function LoginPage() { const [isLoading, setIsLoading] = useState(false); + const [providers, setProviders] = useState([]); - const handleLogin = async () => { + useEffect(() => { + fetch("/api/auth/providers") + .then((r) => r.json()) + .then((data) => setProviders(data.providers ?? [])) + .catch(() => setProviders([])); + }, []); + + const handleSocialLogin = async (provider: string) => { setIsLoading(true); - await signIn.social({ provider: "authentik", callbackURL: window.location.origin }); + await signIn.social({ provider, callbackURL: window.location.origin }); }; + const isGoogle = providers.includes("google"); + const isGitHub = providers.includes("github"); + const isAuthentik = providers.includes("authentik"); + return (
Sign in to continue

- + {isGoogle && ( + + )} + {isGitHub && ( + + )} + {isAuthentik && ( + + )}
); -- 2.52.0