github 0auth fixes
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
# Auth Proxy — Production Ingress Requirement
|
||||
|
||||
The application uses **same-origin authentication**: the browser and React Router SSR both hit
|
||||
`/api/auth/*` on the public application domain. This keeps cookies first-party, avoids
|
||||
cross-origin credentials, and gives development and production the same API surface.
|
||||
The application uses **same-origin authentication**: the browser and React Router SSR both hit `/api/auth/*` on the public application domain. This keeps cookies first-party, avoids cross-origin credentials, and gives development and production the same API surface.
|
||||
|
||||
## Required production route
|
||||
|
||||
At the public application domain, route:
|
||||
|
||||
| Prefix | Target |
|
||||
|---|---|
|
||||
| `/api/auth/*` | Convex HTTP site |
|
||||
| `/*` | React Router frontend |
|
||||
| Prefix | Target |
|
||||
| ------------- | --------------------- |
|
||||
| `/api/auth/*` | Convex HTTP site |
|
||||
| `/*` | React Router frontend |
|
||||
|
||||
### Caddy
|
||||
|
||||
@@ -31,8 +29,7 @@ zopu.example.com {
|
||||
|
||||
### Dokploy / Traefik
|
||||
|
||||
Create a higher-priority path router for `/api/auth` that forwards to the external Convex
|
||||
site URL. Ensure it:
|
||||
Create a higher-priority path router for `/api/auth` that forwards to the external Convex site URL. Ensure it:
|
||||
|
||||
- Preserves the original browser `Cookie` header
|
||||
- Passes `Set-Cookie` responses back (rewrite domain if Convex emits an explicit one)
|
||||
@@ -44,8 +41,7 @@ site URL. Ensure it:
|
||||
|
||||
## Convex environment
|
||||
|
||||
The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex
|
||||
site URL:
|
||||
The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex site URL:
|
||||
|
||||
```bash
|
||||
# Production
|
||||
@@ -58,15 +54,20 @@ npx convex env set SITE_URL 'http://100.101.157.28:5173'
|
||||
npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf'
|
||||
```
|
||||
|
||||
This value populates Better Auth's `trustedOrigins`. The Convex deployment also uses
|
||||
`CONVEX_SITE_URL` as the internal `baseURL` for route registration; that value is the HTTP
|
||||
site URL (e.g. `https://befitting-dalmatian-161.convex.site`).
|
||||
This value is Better Auth's public `baseURL` and `trustedOrigins`. It must be the browser origin because Better Auth writes OAuth state cookies and receives provider callbacks through the same-origin proxy. `CONVEX_SITE_URL` remains the internal Convex HTTP site URL.
|
||||
|
||||
## One origin per shared OAuth deployment
|
||||
|
||||
GitHub OAuth Apps expose one authorization callback URL. The value must exactly equal `<SITE_URL>/api/auth/callback/github`, including scheme, host, port, and path. A GitHub authorization request whose `redirect_uri` differs returns `Invalid Redirect URI` before the user can grant access.
|
||||
|
||||
`SITE_URL` is also single-valued in a Convex deployment. Therefore, two browser origins that share one deployment—such as Tailscale development and Puter staging—cannot use GitHub OAuth at the same time. Either switch both the Convex `SITE_URL` and the GitHub OAuth App callback together before testing the other origin, or give each public environment its own Convex deployment and OAuth App.
|
||||
|
||||
Never set Better Auth's `baseURL` to `CONVEX_SITE_URL` for the browser flow. The browser creates the OAuth state cookie on `SITE_URL`; a provider callback sent directly to `CONVEX_SITE_URL` cannot read that cookie and fails with `state_mismatch`.
|
||||
|
||||
## Why same-origin
|
||||
|
||||
1. **First-party cookies.** No `SameSite=None`, no third-party cookie restrictions.
|
||||
2. **SSR consistency.** The React Router server uses the same auth surface the browser
|
||||
sees; no cross-host credential translation.
|
||||
2. **SSR consistency.** The React Router server uses the same auth surface the browser sees; no cross-host credential translation.
|
||||
3. **No CORS complexity.** The browser talks only to its own origin.
|
||||
4. **The reverse proxy handles the cross-host hop server-side.**
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Deployment Notes
|
||||
|
||||
Two active environments share one Convex deployment (`dev:befitting-dalmatian-161`).
|
||||
Each runs its own web server, Flue agents process, and `.env` file.
|
||||
Two active environments share one Convex deployment (`dev:befitting-dalmatian-161`). Each runs its own web server, Flue agents process, and `.env` file.
|
||||
|
||||
## Environments
|
||||
|
||||
| | Local Mac Dev | Cheaptricks Staging |
|
||||
| | Local Mac Dev | Cheaptricks Staging |
|
||||
| --- | --- | --- |
|
||||
| SSH alias | (local) | `cheaptricks` |
|
||||
| Repo path | `/Users/puter/Workspace/zopu/code` | `/workspace/code` |
|
||||
@@ -19,12 +18,9 @@ Each runs its own web server, Flue agents process, and `.env` file.
|
||||
|
||||
## Shared Convex deployment
|
||||
|
||||
Deployment name: `dev:befitting-dalmatian-161`
|
||||
Convex URL: `https://befitting-dalmatian-161.convex.cloud`
|
||||
Convex Site URL: `https://befitting-dalmatian-161.convex.site`
|
||||
Deployment name: `dev:befitting-dalmatian-161` Convex URL: `https://befitting-dalmatian-161.convex.cloud` Convex Site URL: `https://befitting-dalmatian-161.convex.site`
|
||||
|
||||
Convex env vars are deployment-scoped, not per-machine. Authenticate from any
|
||||
machine with `npx convex dev` inside `packages/backend`.
|
||||
Convex env vars are deployment-scoped, not per-machine. Authenticate from any machine with `npx convex dev` inside `packages/backend`.
|
||||
|
||||
Key Convex env var:
|
||||
|
||||
@@ -32,9 +28,7 @@ Key Convex env var:
|
||||
SITE_URL = <the origin Better Auth should trust>
|
||||
```
|
||||
|
||||
This controls `trustedOrigins` in the Better Auth config
|
||||
(`packages/backend/convex/auth.ts`). It must match the URL the browser
|
||||
actually visits, or sign-in fails silently with a CORS rejection.
|
||||
This controls `trustedOrigins` in the Better Auth config (`packages/backend/convex/auth.ts`). It must match the URL the browser actually visits, or sign-in fails silently with a CORS rejection.
|
||||
|
||||
When switching between environments:
|
||||
|
||||
@@ -48,6 +42,10 @@ npx convex env set SITE_URL 'http://100.101.157.28:5173'
|
||||
npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf'
|
||||
```
|
||||
|
||||
### OAuth consequence
|
||||
|
||||
`SITE_URL` drives both Better Auth's public callback origin and the single GitHub OAuth App callback. When switching the shared deployment between the local and staging origins, also update the GitHub OAuth App's **Authorization callback URL** to `<SITE_URL>/api/auth/callback/github` before attempting a GitHub connection. GitHub OAuth Apps allow one callback URL; use distinct Convex deployments and OAuth Apps if both environments must operate concurrently.
|
||||
|
||||
## Local Mac Dev `.env`
|
||||
|
||||
```env
|
||||
@@ -143,16 +141,24 @@ zopu.cheaptricks.puter.wtf {
|
||||
bind 135.181.82.179 2a01:4f9:c013:4a64::1
|
||||
encode zstd gzip
|
||||
|
||||
# Must precede the generic /api route: preserves /api/auth/* and cookies.
|
||||
handle /api/auth/* {
|
||||
reverse_proxy https://befitting-dalmatian-161.convex.site {
|
||||
header_up Host befitting-dalmatian-161.convex.site
|
||||
}
|
||||
}
|
||||
|
||||
handle_path /api/* {
|
||||
reverse_proxy 127.0.0.1:3585
|
||||
}
|
||||
|
||||
reverse_proxy 127.0.0.1:5173
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:5173
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`/api/*` routes to the Flue agents process (port 3585).
|
||||
Everything else routes to the web dev server (port 5173).
|
||||
`/api/auth/*` is the same-origin Better Auth proxy to Convex. The generic `/api/*` route serves Flue at port `3585`; it must be evaluated only after the authentication route.
|
||||
|
||||
Reload after changes:
|
||||
|
||||
@@ -162,8 +168,7 @@ sudo systemctl reload caddy
|
||||
|
||||
### Vite allowedHosts
|
||||
|
||||
`apps/web/vite.config.ts` must include `server: { allowedHosts: true }` or
|
||||
Vite rejects requests arriving through the Caddy domain.
|
||||
`apps/web/vite.config.ts` must include `server: { allowedHosts: true }` or Vite rejects requests arriving through the Caddy domain.
|
||||
|
||||
### Start staging dev
|
||||
|
||||
@@ -211,23 +216,18 @@ Both environments use the same model via the Cheaptricks AI gateway:
|
||||
- Multimodal: text + image input
|
||||
|
||||
The `AGENT_MODEL_BASE_URL` differs:
|
||||
|
||||
- Local Mac: uses the external gateway URL
|
||||
- Cheaptricks: uses `https://ai.cheaptricks.puter.wtf/v1` (local to the box)
|
||||
|
||||
## Known gotchas
|
||||
|
||||
1. **Convex SITE_URL is single-valued.** Only one origin can be trusted at a
|
||||
time. Switch it when moving between local and staging, or sign-in breaks
|
||||
silently with a CORS error in the browser console.
|
||||
1. **Convex `SITE_URL` is single-valued and is the OAuth callback origin.** Switch it together with the GitHub OAuth App's single callback URL when moving between local and staging. Running both origins concurrently requires separate Convex deployments and OAuth Apps.
|
||||
|
||||
2. **Vite blocks unknown hosts by default.** Caddy domain must be allowed
|
||||
via `server.allowedHosts` in `apps/web/vite.config.ts`.
|
||||
2. **Vite blocks unknown hosts by default.** Caddy domain must be allowed via `server.allowedHosts` in `apps/web/vite.config.ts`.
|
||||
|
||||
3. **Flue port changed from 3583 to 3585.** The old Caddy config pointed at
|
||||
3583/13100. Current ports are 3585 (Flue) and 5173 (web).
|
||||
3. **Flue port changed from 3583 to 3585.** The old Caddy config pointed at 3583/13100. Current ports are 3585 (Flue) and 5173 (web).
|
||||
|
||||
4. **`.env` is gitignored.** Each machine maintains its own copy. The repo
|
||||
ships `.env.example` as the template.
|
||||
4. **`.env` is gitignored.** Each machine maintains its own copy. The repo ships `.env.example` as the template.
|
||||
|
||||
5. **Convex CLI auth is per-machine.** Run `npx convex dev` once inside
|
||||
`packages/backend` on each new machine to authenticate the CLI.
|
||||
5. **Convex CLI auth is per-machine.** Run `npx convex dev` once inside `packages/backend` on each new machine to authenticate the CLI.
|
||||
|
||||
@@ -6,20 +6,27 @@ This document covers manual setup for GitHub OAuth, Puter Git (Gitea), webhooks,
|
||||
|
||||
## GitHub OAuth Application
|
||||
|
||||
1. Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App.
|
||||
2. Set the application name (e.g., "Zopu").
|
||||
3. Set the homepage URL to your `SITE_URL` (e.g., `https://zopu.cheaptricks.puter.wtf`).
|
||||
4. Set the callback URL to `<SITE_URL>/api/auth/callback/github`.
|
||||
5. Generate a client secret.
|
||||
6. Set Convex environment variables:
|
||||
1. Go to GitHub **Settings → Developer settings → OAuth Apps** and open the app whose client ID will be stored in Convex.
|
||||
2. Set the application name and homepage URL. The homepage URL should be the current `SITE_URL`.
|
||||
3. Set the single **Authorization callback URL** to exactly:
|
||||
```text
|
||||
<SITE_URL>/api/auth/callback/github
|
||||
```
|
||||
GitHub OAuth Apps support one callback URL. Scheme, host, port, and path must match the `redirect_uri` sent by Better Auth exactly.
|
||||
4. Generate a client secret.
|
||||
5. Set the credentials on the Convex deployment—not only in the local `.env` file:
|
||||
```bash
|
||||
cd packages/backend
|
||||
npx convex env set GITHUB_CLIENT_ID <your-client-id>
|
||||
npx convex env set GITHUB_CLIENT_SECRET <your-client-secret>
|
||||
npx convex env list
|
||||
```
|
||||
|
||||
The configured `SITE_URL`, GitHub callback URL, and browser URL are one unit. For a shared Convex deployment, switch all three before testing a different public origin. See [Auth Proxy](./auth-proxy.md#one-origin-per-shared-oauth-deployment) for the deployment boundary.
|
||||
|
||||
### Requested scopes
|
||||
|
||||
Zopu requests `repo` and `read:org`. The `repo` scope grants access to private repositories. `read:org` allows listing organization repositories.
|
||||
Zopu requests GitHub identity and private-repository access: `read:user`, `user:email`, `repo`, and `read:org`. `repo` grants access to private repositories; `read:org` permits organization repository discovery.
|
||||
|
||||
## GitHub Webhook (manual setup for OAuth-based version)
|
||||
|
||||
@@ -102,6 +109,18 @@ Better Auth requires the callback URL to be on the same origin as `SITE_URL`. If
|
||||
2. Complete the GitHub OAuth flow.
|
||||
3. Verify the connection state shows `active`.
|
||||
|
||||
### OAuth diagnostics
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `Provider not found` from `/api/auth/link-social` | `GITHUB_CLIENT_ID` or `GITHUB_CLIENT_SECRET` is absent from the Convex deployment env store. A local `.env` does not configure cloud functions. | Set both variables with `npx convex env set` and verify with `npx convex env list`. |
|
||||
| GitHub `Invalid Redirect URI` | GitHub's saved callback does not exactly equal the app's `redirect_uri`. | Set the OAuth App's single callback to `<SITE_URL>/api/auth/callback/github`. |
|
||||
| `state_mismatch` at a `convex.site` page | Better Auth started on the public origin but GitHub returned to the direct Convex host, so the public-origin state cookie is unavailable. | Set Better Auth `baseURL` to `SITE_URL`; proxy `/api/auth/*` through the public origin. |
|
||||
| `email_doesn't_match` after selecting GitHub | A deployment predates the explicit-link policy or has not received the current auth configuration. | Deploy the current backend. Explicit `linkSocial` allows a different GitHub email without changing the Zopu account email. |
|
||||
| `externalEmail` is `null` during `connectGithub` | The GitHub account hides its public email; GitHub returns `null` from `/user`. | Deploy the current backend, which normalizes nullable provider emails before Convex persistence. |
|
||||
|
||||
GitHub connections are explicit authenticated account links. The linked GitHub email may differ from the existing Zopu account email; Zopu retains the existing account email and only stores the GitHub identity and OAuth credential needed for repository access.
|
||||
|
||||
### Reconnection
|
||||
|
||||
If a token is expired or revoked:
|
||||
|
||||
@@ -17,8 +17,13 @@ export const authComponent = createClient<DataModel>(components.betterAuth);
|
||||
|
||||
const createAuth = (ctx: GenericCtx<DataModel>) =>
|
||||
betterAuth({
|
||||
account: {
|
||||
accountLinking: {
|
||||
allowDifferentEmails: true,
|
||||
},
|
||||
},
|
||||
advanced: { useSecureCookies: siteUrl.startsWith("https://") },
|
||||
baseURL: env.CONVEX_SITE_URL,
|
||||
baseURL: siteUrl,
|
||||
database: authComponent.adapter(ctx),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
|
||||
@@ -80,13 +80,13 @@ const fetchGiteaUser = async (
|
||||
);
|
||||
}
|
||||
const user = (await response.json()) as {
|
||||
readonly email?: string;
|
||||
readonly email?: string | null;
|
||||
readonly id: number;
|
||||
readonly login: string;
|
||||
};
|
||||
return {
|
||||
externalAccountId: String(user.id),
|
||||
externalEmail: user.email,
|
||||
externalEmail: user.email ?? undefined,
|
||||
externalUsername: user.login,
|
||||
};
|
||||
};
|
||||
@@ -105,13 +105,13 @@ const fetchGithubUser = async (token: string): Promise<ExternalUserInfo> => {
|
||||
);
|
||||
}
|
||||
const user = (await response.json()) as {
|
||||
readonly email?: string;
|
||||
readonly email?: string | null;
|
||||
readonly id: number;
|
||||
readonly login: string;
|
||||
};
|
||||
return {
|
||||
externalAccountId: String(user.id),
|
||||
externalEmail: user.email,
|
||||
externalEmail: user.email ?? undefined,
|
||||
externalUsername: user.login,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user