diff --git a/cloud/.env.example b/cloud/.env.example index 58df7664..d334935e 100644 --- a/cloud/.env.example +++ b/cloud/.env.example @@ -40,6 +40,12 @@ AUTH_URL="http://localhost:3000" AUTH_GITHUB_ID="" AUTH_GITHUB_SECRET="" +# Google OAuth (also optional in dev, same reason). Offer either, both, or +# neither — each is registered independently based on which vars are set. +# Create an OAuth client: https://console.cloud.google.com/apis/credentials +AUTH_GOOGLE_ID="" +AUTH_GOOGLE_SECRET="" + # S3-compatible object storage for run screenshots + recording traces. # Works with AWS S3, Cloudflare R2, or MinIO. Not required for local dev. # diff --git a/cloud/apps/web/src/app/(app)/dashboard/page.tsx b/cloud/apps/web/src/app/(app)/dashboard/page.tsx index d3111d0f..1d062822 100644 --- a/cloud/apps/web/src/app/(app)/dashboard/page.tsx +++ b/cloud/apps/web/src/app/(app)/dashboard/page.tsx @@ -1,14 +1,13 @@ import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card"; -import { EnqueueNoopButton } from "@/components/enqueue-noop-button"; export const dynamic = "force-dynamic"; -const PILLARS = [ - { n: 1, label: "Record a browser workflow", status: "Phase 2" }, - { n: 2, label: "Convert it into editable steps", status: "Phase 2" }, - { n: 3, label: "Replay across browser / API actions", status: "Phase 1" }, - { n: 4, label: "Approve sensitive actions", status: "Phase 1" }, - { n: 5, label: "Log every run with verification", status: "Phase 1" }, +const CAPABILITIES = [ + "Record a browser workflow", + "Convert it into editable steps", + "Replay across browser / API actions", + "Approve sensitive actions before they execute", + "Verify the outcome and log every run", ]; export default function DashboardPage() { @@ -24,34 +23,16 @@ export default function DashboardPage() { - MVP capabilities + What Ghost does - {PILLARS.map((p) => ( -
- - {p.n}. - {p.label} - - - {p.status} - + {CAPABILITIES.map((label) => ( +
+ {label}
))} - - - - Wiring check - - -

- Enqueue a no-op job to confirm the web → Redis → worker path is live. -

- -
-
); } diff --git a/cloud/apps/web/src/app/signin/page.tsx b/cloud/apps/web/src/app/signin/page.tsx index 62d71524..f77fc37f 100644 --- a/cloud/apps/web/src/app/signin/page.tsx +++ b/cloud/apps/web/src/app/signin/page.tsx @@ -5,13 +5,15 @@ import { Card, CardBody } from "@/components/ui/card"; import { SOURCE_URL } from "@/lib/source-url"; const githubEnabled = Boolean(process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET); +const googleEnabled = Boolean(process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET); const devEnabled = process.env.NODE_ENV !== "production"; -// Both false means production with no OAuth provider configured: no form -// below has anything to render, and a card with a title and no buttons looks -// like a bug rather than a missing deploy step. Whoever hits this is more -// likely to be the person standing up the deployment than an end user, so -// name the exact fix rather than failing silently. See docs/DEPLOY.md's "sign-in trap". -const misconfigured = !githubEnabled && !devEnabled; +// All three false means production with no OAuth provider configured: no +// form below has anything to render, and a card with a title and no buttons +// looks like a bug rather than a missing deploy step. Whoever hits this is +// more likely to be the person standing up the deployment than an end user, +// so name the exact fix rather than failing silently. See docs/DEPLOY.md's +// "sign-in trap". +const misconfigured = !githubEnabled && !googleEnabled && !devEnabled; export default async function SignInPage({ searchParams, @@ -42,12 +44,13 @@ export default async function SignInPage({ No sign-in method is configured

- This deployment has NODE_ENV=production and no GitHub OAuth - app configured, so there is no way to sign in. Set{" "} - AUTH_GITHUB_ID and AUTH_GITHUB_SECRET, with the + This deployment has NODE_ENV=production and no OAuth app + configured, so there is no way to sign in. Set either{" "} + AUTH_GITHUB_ID/AUTH_GITHUB_SECRET or{" "} + AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET, with the app's callback at{" "} - https://<this-domain>/api/auth/callback/github. See - docs/DEPLOY.md. + https://<this-domain>/api/auth/callback/<github|google>. + See docs/DEPLOY.md.

)} @@ -65,6 +68,19 @@ export default async function SignInPage({ )} + {googleEnabled && ( +
{ + "use server"; + await signIn("google", { redirectTo }); + }} + > + +
+ )} + {devEnabled && (
{ diff --git a/cloud/apps/web/src/auth.ts b/cloud/apps/web/src/auth.ts index ef9263ee..80fa3682 100644 --- a/cloud/apps/web/src/auth.ts +++ b/cloud/apps/web/src/auth.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import NextAuth, { type NextAuthConfig } from "next-auth"; import Credentials from "next-auth/providers/credentials"; import GitHub from "next-auth/providers/github"; +import Google from "next-auth/providers/google"; import { assertAuthSecretUsable, devSignInAllowed, sessionMaxAgeSeconds } from "@/lib/auth-env"; import { ensureUserOrg } from "@/lib/org"; @@ -13,8 +14,10 @@ import { ensureUserOrg } from "@/lib/org"; * (`ensureUserOrg`) and stamp `userId`/`orgId` into the token, so every request * is scoped to a tenant. * - * GitHub OAuth is enabled only when its env vars are present. Locally, the - * "Dev sign-in" provider accepts any email. + * GitHub and Google OAuth are each enabled only when their own env vars are + * present — a deployment can offer one, both, or neither (falling back to the + * dev-only provider below). Locally, the "Dev sign-in" provider accepts any + * email. */ // Before anything else: in production, refuse to start on a session secret @@ -27,6 +30,10 @@ if (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) { providers.push(GitHub); } +if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) { + providers.push(Google); +} + // Passwordless "any email" sign-in for local development. `devSignInAllowed` // requires production to be ruled out AND the instance to be loopback-only, // so a deployment with NODE_ENV unset does not quietly expose it. diff --git a/cloud/apps/web/src/components/enqueue-noop-button.tsx b/cloud/apps/web/src/components/enqueue-noop-button.tsx deleted file mode 100644 index 3ed52fe2..00000000 --- a/cloud/apps/web/src/components/enqueue-noop-button.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "@/components/ui/button"; - -export function EnqueueNoopButton() { - const [state, setState] = useState<"idle" | "loading" | "done" | "error">("idle"); - const [detail, setDetail] = useState(""); - - async function enqueue() { - setState("loading"); - setDetail(""); - try { - const res = await fetch("/api/dev/enqueue-noop", { method: "POST" }); - const body = await res.json(); - if (!res.ok) throw new Error(body?.error ?? "failed"); - setState("done"); - setDetail(`Job ${body.jobId} queued — check the worker logs.`); - } catch (err) { - setState("error"); - setDetail(err instanceof Error ? err.message : "failed"); - } - } - - return ( -
- - {detail && ( - - {detail} - - )} -
- ); -} diff --git a/cloud/docs/DEPLOY.md b/cloud/docs/DEPLOY.md index d367ecee..c575925c 100644 --- a/cloud/docs/DEPLOY.md +++ b/cloud/docs/DEPLOY.md @@ -51,7 +51,8 @@ B2, or MinIO. | `DATABASE_URL` | ● | ● | nothing works | | `REDIS_URL` | ● | ● | runs queue and never execute | | `AUTH_SECRET` | ● | | sessions cannot be signed | -| `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` | ● | | **no way to sign in at all** — see below | +| `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` | ● | | one fewer sign-in option; **no way to sign in at all** if Google is also unset — see below | +| `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` | ● | | same as above, independently — offer either, both, or neither | | `S3_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | ● | ● | silent fallback to local disk; artifacts lost | | `S3_REGION`, `S3_ENDPOINT` | ○ | ○ | needed for non-AWS S3-compatible stores | | `GHOST_SESSION_KEY` | | ● | gated runs cannot resume after approval | @@ -67,10 +68,15 @@ reason to hold that power. ### The sign-in trap `apps/web/src/app/signin/page.tsx` shows the dev email form only when -`NODE_ENV !== "production"`, and the GitHub button only when both GitHub -variables are set. In production with neither configured, the page renders with -**no way to sign in**. Configure the GitHub OAuth app before the first deploy, -with its callback at `https:///api/auth/callback/github`. +`NODE_ENV !== "production"`, the GitHub button only when both GitHub +variables are set, and the Google button only when both Google variables are +set — each provider is independent. In production with all three unset, the +page renders an explicit "no sign-in method is configured" error rather than +silently showing nothing (see `signin/page.tsx`'s `misconfigured` check), but +that is a diagnostic, not a fix: configure at least one real OAuth app before +the first deploy, with its callback at +`https:///api/auth/callback/github` or +`https:///api/auth/callback/google`. ### The domain trap @@ -147,6 +153,19 @@ only one side gets them. `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET`. See "The sign-in trap" above for what happens if you skip this. +**Google OAuth client** (console.cloud.google.com/apis/credentials → Create +Credentials → OAuth client ID → Web application) — do this too, not instead +of GitHub: GitHub-only sign-in excludes anyone in Ghost's actual target +market (ops-heavy SMBs) who doesn't have or want a GitHub account: +- Authorized redirect URI: `https:///api/auth/callback/google` + — same exact-match rule as GitHub's callback. +- The OAuth consent screen needs to exist first (Google requires basic app + info — name, support email — before it issues credentials); "Testing" mode + is fine until you need non-allowlisted users to sign in, at which point it + needs Google's verification review. +- Note the Client ID and Client Secret; these become `AUTH_GOOGLE_ID` / + `AUTH_GOOGLE_SECRET`. + **Vercel project for `cloud/apps/web`** — this is "The domain trap" above made concrete: 1. Create a **new, second** Vercel project. Do not add `cloud/apps/web` to @@ -176,7 +195,8 @@ concrete: marketing site's). 6. Set every `web`-column environment variable from the table above on this project (`DATABASE_URL`, `REDIS_URL`, `AUTH_SECRET`, `AUTH_GITHUB_ID`/ - `AUTH_GITHUB_SECRET`, the `S3_*` set, `APP_URL`). + `AUTH_GITHUB_SECRET`, `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET`, the `S3_*` + set, `APP_URL`). 7. **Disable Vercel's own Deployment Protection (SSO/Vercel Authentication)** for this project, or it gates every page — including `/signin` — behind a Vercel-account login wall on top of Ghost's own auth, blocking real users