Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cloud/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
39 changes: 10 additions & 29 deletions cloud/apps/web/src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -24,34 +23,16 @@ export default function DashboardPage() {

<Card>
<CardHeader>
<CardTitle>MVP capabilities</CardTitle>
<CardTitle>What Ghost does</CardTitle>
</CardHeader>
<CardBody className="space-y-2">
{PILLARS.map((p) => (
<div key={p.n} className="flex items-center justify-between text-sm">
<span>
<span className="mr-2 text-[var(--color-muted)]">{p.n}.</span>
{p.label}
</span>
<span className="rounded-md border border-[var(--color-border)] px-2 py-0.5 text-xs text-[var(--color-muted)]">
{p.status}
</span>
{CAPABILITIES.map((label) => (
<div key={label} className="text-sm">
{label}
</div>
))}
</CardBody>
</Card>

<Card>
<CardHeader>
<CardTitle>Wiring check</CardTitle>
</CardHeader>
<CardBody className="space-y-3">
<p className="text-sm text-[var(--color-muted)]">
Enqueue a no-op job to confirm the web → Redis → worker path is live.
</p>
<EnqueueNoopButton />
</CardBody>
</Card>
</div>
);
}
38 changes: 27 additions & 11 deletions cloud/apps/web/src/app/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,12 +44,13 @@ export default async function SignInPage({
No sign-in method is configured
</p>
<p className="text-[var(--color-muted)]">
This deployment has <code>NODE_ENV=production</code> and no GitHub OAuth
app configured, so there is no way to sign in. Set{" "}
<code>AUTH_GITHUB_ID</code> and <code>AUTH_GITHUB_SECRET</code>, with the
This deployment has <code>NODE_ENV=production</code> and no OAuth app
configured, so there is no way to sign in. Set either{" "}
<code>AUTH_GITHUB_ID</code>/<code>AUTH_GITHUB_SECRET</code> or{" "}
<code>AUTH_GOOGLE_ID</code>/<code>AUTH_GOOGLE_SECRET</code>, with the
app&apos;s callback at{" "}
<code>https://&lt;this-domain&gt;/api/auth/callback/github</code>. See
docs/DEPLOY.md.
<code>https://&lt;this-domain&gt;/api/auth/callback/&lt;github|google&gt;</code>.
See docs/DEPLOY.md.
</p>
</div>
)}
Expand All @@ -65,6 +68,19 @@ export default async function SignInPage({
</form>
)}

{googleEnabled && (
<form
action={async () => {
"use server";
await signIn("google", { redirectTo });
}}
>
<Button type="submit" variant="secondary" className="w-full">
Continue with Google
</Button>
</form>
)}

{devEnabled && (
<form
action={async (formData: FormData) => {
Expand Down
11 changes: 9 additions & 2 deletions cloud/apps/web/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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
Expand All @@ -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.
Expand Down
43 changes: 0 additions & 43 deletions cloud/apps/web/src/components/enqueue-noop-button.tsx

This file was deleted.

32 changes: 26 additions & 6 deletions cloud/docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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://<your-app-domain>/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://<your-app-domain>/api/auth/callback/github` or
`https://<your-app-domain>/api/auth/callback/google`.

### The domain trap

Expand Down Expand Up @@ -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://<your-app-domain>/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
Expand Down Expand Up @@ -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
Expand Down
Loading