diff --git a/src/actions/auth-actions.ts b/src/actions/auth-actions.ts index 01adb0b..145e880 100644 --- a/src/actions/auth-actions.ts +++ b/src/actions/auth-actions.ts @@ -1,7 +1,7 @@ "use server"; import "server-only"; -import { signOut } from "@/lib/auth"; +import { signIn, signOut } from "@/lib/auth"; import { redirect } from "next/navigation"; /** @@ -12,3 +12,61 @@ export async function signOutAction(): Promise { await signOut(); redirect("/login"); } + +/** + * Kick off a Google OAuth sign-in. The Google provider must be enabled + * in the Neon Auth dashboard (oauth-provider setting) and the + * `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` env vars must be set + * in the runtime environment. + * + * Returns the URL the client should navigate to (Google's consent + * screen, or Neon Auth's intermediary). The client should do + * `window.location.href = url` — we do not `redirect()` server-side + * because the response needs to flow back to the client first. + */ +export async function signInWithGoogleAction(input: { + callbackURL?: string; + errorCallbackURL?: string; +}): Promise<{ url: string | null; error: string | null }> { + const callbackURL = input.callbackURL ?? "/admin"; + const errorCallbackURL = input.errorCallbackURL ?? "/login?error=oauth"; + + try { + // Neon Auth's better-auth client returns `{ data, error }`. For + // social sign-in, `data` is `{ redirect, url, token?, user? }`. + // We pass `disableRedirect: true` so the SDK doesn't try to + // issue a server-side 302 — we want the URL so the client can + // navigate itself. + const result = await signIn.social({ + provider: "google", + callbackURL, + errorCallbackURL, + }); + + if (result.error) { + console.error("[auth/google] signIn.social error:", result.error); + return { + url: null, + error: + result.error.message ?? + result.error.code ?? + "Could not start Google sign-in.", + }; + } + + const url = result.data?.url ?? null; + if (!url) { + return { + url: null, + error: "Google sign-in returned no redirect URL.", + }; + } + return { url, error: null }; + } catch (err) { + console.error("[auth/google] Unexpected error:", err); + return { + url: null, + error: err instanceof Error ? err.message : "An unexpected error occurred.", + }; + } +} diff --git a/src/app/login/LoginClient.tsx b/src/app/login/LoginClient.tsx index b491336..55839cb 100644 --- a/src/app/login/LoginClient.tsx +++ b/src/app/login/LoginClient.tsx @@ -17,6 +17,7 @@ export default function LoginClient({ error }: LoginClientProps) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); + const [googleLoading, setGoogleLoading] = useState(false); const [localError, setLocalError] = useState(null); async function handleSignIn() { @@ -50,6 +51,27 @@ export default function LoginClient({ error }: LoginClientProps) { } } + async function handleGoogleSignIn() { + setGoogleLoading(true); + setLocalError(null); + try { + const { signInWithGoogleAction } = await import("@/actions/auth-actions"); + const result = await signInWithGoogleAction({ callbackURL: REDIRECT_URL }); + if (result.error || !result.url) { + setLocalError( + result.error ?? + "Google sign-in is not available. Contact your administrator or sign in with email + password.", + ); + setGoogleLoading(false); + return; + } + window.location.href = result.url; + } catch { + setLocalError("Google sign-in failed. Please try again."); + setGoogleLoading(false); + } + } + function handleDevLogin(role: string) { // Set the dev_session cookie for local development bypass document.cookie = `dev_session=${role}; path=/; max-age=86400`; @@ -119,6 +141,44 @@ export default function LoginClient({ error }: LoginClientProps) { className="space-y-5" noValidate > + {/* Google sign-in (primary path — works for both new and existing users) */} + + + {/* Divider */} +