@clubedge/create-clubedge-app 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +11 -10
- package/package.json +3 -3
- package/template/.env.example +21 -0
- package/template/.github/workflows/ci.yml +11 -0
- package/template/README.md +11 -1
- package/template/SETUP.md +37 -0
- package/template/apps/start/.env.example +21 -0
- package/template/apps/start/package.json +3 -1
- package/template/apps/start/src/env/auth.ts +66 -1
- package/template/apps/start/src/env/production.ts +19 -0
- package/template/apps/start/src/routes/reset-password.tsx +3 -1
- package/template/apps/start/src/server/auth-email.ts +2 -2
- package/template/apps/start/src/server/auth-forms.ts +3 -1
- package/template/apps/start/src/server/variants/auth.appwrite.ts +42 -0
- package/template/apps/start/src/server/variants/auth.firebase.ts +49 -0
- package/template/apps/web/package.json +3 -1
- package/template/apps/web/src/app/auth/callback/route.ts +3 -1
- package/template/apps/web/src/app/reset-password/page.tsx +6 -2
- package/template/apps/web/src/env/auth.ts +67 -0
- package/template/apps/web/src/env/production.ts +19 -0
- package/template/apps/web/src/server/auth-email.ts +2 -2
- package/template/apps/web/src/server/variants/auth.appwrite.ts +48 -0
- package/template/apps/web/src/server/variants/auth.firebase.ts +55 -0
- package/template/clubedge.template.json +97 -1
- package/template/firebase.json +7 -0
- package/template/package.json +3 -2
- package/template/packages/auth/package.json +3 -2
- package/template/packages/auth/src/conformance.ts +322 -0
- package/template/packages/{auth-better-auth → auth}/src/emails.ts +2 -1
- package/template/packages/auth/src/index.ts +7 -0
- package/template/packages/auth/src/links.test.ts +22 -0
- package/template/packages/auth/src/links.ts +28 -0
- package/template/packages/auth-appwrite/package.json +23 -0
- package/template/packages/auth-appwrite/src/conformance.live.test.ts +82 -0
- package/template/packages/auth-appwrite/src/index.test.ts +204 -0
- package/template/packages/auth-appwrite/src/index.ts +202 -0
- package/template/packages/auth-appwrite/tsconfig.json +4 -0
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-better-auth/src/conformance.test.ts +49 -0
- package/template/packages/auth-better-auth/src/index.ts +11 -3
- package/template/packages/auth-firebase/package.json +23 -0
- package/template/packages/auth-firebase/src/conformance.test.ts +57 -0
- package/template/packages/auth-firebase/src/index.ts +310 -0
- package/template/packages/auth-firebase/tsconfig.json +4 -0
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/auth-supabase/src/conformance.test.ts +71 -0
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/drizzle/0001_auth_user_id_text.sql +3 -0
- package/template/packages/db/drizzle/meta/_journal.json +7 -0
- package/template/packages/db/package.json +1 -1
- package/template/packages/db/src/schema/users.ts +3 -2
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-bullmq/package.json +1 -1
- package/template/packages/jobs-inngest/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +793 -0
- package/template/supabase/config.toml +4 -0
- package/template.lock.json +23 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.32.0
|
|
6
|
+
|
|
7
|
+
- **Pluggable authentication**: default to Starter `v0.21.0` (#93). Authentication now has four providers behind one interface, so the pages, settings, payments, and jobs stay the same whichever signs users in:
|
|
8
|
+
- `--auth supabase` and `--auth better-auth`, as before.
|
|
9
|
+
- **`--auth firebase`** (new): Firebase Authentication from the server, with session cookies. `pnpm auth:dev` starts the Auth Emulator, so development needs no Firebase account. Reset and confirmation emails go through the app's email module.
|
|
10
|
+
- **`--auth appwrite`** (new): Appwrite accounts on Appwrite Cloud or your own server, with session cookies and an API key. Appwrite sends its own emails.
|
|
11
|
+
- `@clubedge/auth/conformance` is one test suite every auth provider passes: sign-up and sign-in, sign-out, reset through the emailed link, password changes that sign out other sessions, and account deletion. The Starter's CI runs it against all four, including Appwrite Cloud.
|
|
12
|
+
- Stripe payments work with every provider.
|
|
13
|
+
- Switch an existing project with `pnpm clubedge add auth firebase` or `pnpm clubedge add auth appwrite`. After `pnpm clubedge upgrade`, run `pnpm db:migrate`: `users.auth_user_id` becomes text, since Firebase's and Appwrite's user ids aren't UUIDs.
|
|
14
|
+
|
|
5
15
|
## 0.31.0
|
|
6
16
|
|
|
7
17
|
- **Pluggable background jobs**: default to Starter `v0.20.0` (#91). The jobs module now has three providers behind one interface, so job definitions, routes, and the dashboard's jobs card stay the same whichever runs them:
|
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
129
129
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
130
130
|
|
|
131
131
|
# Reproduce a specific Starter release or commit
|
|
132
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
132
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.21.0
|
|
133
133
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
134
134
|
|
|
135
135
|
# Test local Starter changes before releasing them
|
|
@@ -148,7 +148,7 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
148
148
|
pnpm clubedge remove storage payments
|
|
149
149
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
150
150
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
151
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
151
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.21.0 # or a specific tag, branch, or commit
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
|
|
@@ -259,27 +259,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
259
259
|
Each CLI release is tested against one specific Starter release:
|
|
260
260
|
|
|
261
261
|
```text
|
|
262
|
-
create-clubedge-app v0.
|
|
262
|
+
create-clubedge-app v0.32.0
|
|
263
263
|
|
|
|
264
264
|
v
|
|
265
|
-
Clubedge Starter v0.
|
|
266
|
-
commit
|
|
265
|
+
Clubedge Starter v0.21.0
|
|
266
|
+
commit 8f1201f2d526347ccf0365a701aaa20e883f8d8a
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
The current relationship is:
|
|
270
270
|
|
|
271
271
|
| CLI release | Default Starter release | Meaning |
|
|
272
272
|
| --- | --- | --- |
|
|
273
|
-
| `create-clubedge-app@0.
|
|
273
|
+
| `create-clubedge-app@0.32.0` | `clubedge-starter@v0.21.0` | The CLI's required, tested default |
|
|
274
274
|
|
|
275
275
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
276
276
|
|
|
277
|
-
The `v0.
|
|
278
|
-
`
|
|
277
|
+
The `v0.21.0` tag is expected to resolve to commit
|
|
278
|
+
`8f1201f2d526347ccf0365a701aaa20e883f8d8a`. The CLI verifies this before
|
|
279
279
|
scaffolding with the default reference and fails rather than silently using a
|
|
280
280
|
moved tag.
|
|
281
281
|
|
|
282
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
282
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.21.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
|
|
283
283
|
|
|
284
284
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
285
285
|
|
|
@@ -334,7 +334,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
334
334
|
1. Make and validate changes in `clubedge-starter`.
|
|
335
335
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
336
336
|
3. Update the Starter package versions.
|
|
337
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
337
|
+
4. Commit `main`, create a release tag such as `v0.21.0`, and publish the Starter release.
|
|
338
338
|
|
|
339
339
|
### CLI maintainers
|
|
340
340
|
|
|
@@ -376,6 +376,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
376
376
|
| 0.30.0 | v0.19.0 | `--infra neon` with a free Neon database, the `neon` command, `deploy --preview` with a Neon branch per preview |
|
|
377
377
|
| 0.30.1 | v0.19.1 | Railway previews work with a workspace token and answer on the port their domain targets |
|
|
378
378
|
| 0.31.0 | v0.20.0 | Pluggable background jobs: `--jobs bullmq` and `--jobs inngest` next to pg-boss, and a conformance suite every provider passes |
|
|
379
|
+
| 0.32.0 | v0.21.0 | Pluggable authentication: `--auth firebase` and `--auth appwrite` next to Supabase and Better Auth, and a conformance suite every provider passes |
|
|
379
380
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
380
381
|
|
|
381
382
|
## Community
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"clubedge": {
|
|
24
24
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
25
|
-
"starterRef": "v0.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.21.0",
|
|
26
|
+
"starterCommit": "8f1201f2d526347ccf0365a701aaa20e883f8d8a"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/.env.example
CHANGED
|
@@ -35,6 +35,27 @@ BETTER_AUTH_SECRET=
|
|
|
35
35
|
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
36
36
|
# clubedge:end
|
|
37
37
|
|
|
38
|
+
# clubedge:if auth=firebase
|
|
39
|
+
# Firebase Authentication. In development, pnpm auth:dev starts the Auth Emulator, which these
|
|
40
|
+
# values point at. In production, set your project's id and Web API key (Project settings,
|
|
41
|
+
# General) and a service account (Project settings, Service accounts), and remove
|
|
42
|
+
# FIREBASE_AUTH_EMULATOR_HOST.
|
|
43
|
+
FIREBASE_PROJECT_ID=demo-clubedge
|
|
44
|
+
FIREBASE_API_KEY=demo-api-key
|
|
45
|
+
FIREBASE_CLIENT_EMAIL=
|
|
46
|
+
FIREBASE_PRIVATE_KEY=
|
|
47
|
+
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
|
|
48
|
+
# clubedge:end
|
|
49
|
+
|
|
50
|
+
# clubedge:if auth=appwrite
|
|
51
|
+
# Appwrite (https://appwrite.io), on Appwrite Cloud or your own server. Create a project, add a
|
|
52
|
+
# Web platform for this app's hostname (localhost in development), and an API key with the
|
|
53
|
+
# sessions.write and users.write scopes. Appwrite sends its own emails (its SMTP settings).
|
|
54
|
+
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
|
55
|
+
APPWRITE_PROJECT_ID=
|
|
56
|
+
APPWRITE_API_KEY=
|
|
57
|
+
# clubedge:end
|
|
58
|
+
|
|
38
59
|
# clubedge:if cache=redis
|
|
39
60
|
# Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
|
|
40
61
|
# process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
|
|
@@ -22,6 +22,8 @@ jobs:
|
|
|
22
22
|
env:
|
|
23
23
|
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres
|
|
24
24
|
REDIS_URL: redis://127.0.0.1:6379 # clubedge:only jobs=bullmq
|
|
25
|
+
FIREBASE_AUTH_EMULATOR_HOST: 127.0.0.1:9099 # clubedge:only auth=firebase
|
|
26
|
+
FIREBASE_PROJECT_ID: demo-clubedge # clubedge:only auth=firebase
|
|
25
27
|
steps:
|
|
26
28
|
- uses: actions/checkout@v7
|
|
27
29
|
- uses: pnpm/action-setup@v6
|
|
@@ -33,6 +35,15 @@ jobs:
|
|
|
33
35
|
- run: pnpm format:check
|
|
34
36
|
- run: pnpm lint
|
|
35
37
|
- run: pnpm typecheck
|
|
38
|
+
# clubedge:if auth=firebase
|
|
39
|
+
# Firebase's Auth Emulator (firebase.json), for the Firebase provider's conformance tests.
|
|
40
|
+
- name: Start the Firebase Auth Emulator
|
|
41
|
+
run: |
|
|
42
|
+
nohup npx --yes firebase-tools@15 emulators:start --only auth --project demo-clubedge \
|
|
43
|
+
> "$RUNNER_TEMP/firebase.log" 2>&1 &
|
|
44
|
+
timeout 240 bash -c 'until curl --silent --output /dev/null http://127.0.0.1:9099/; do sleep 2; done' \
|
|
45
|
+
|| { cat "$RUNNER_TEMP/firebase.log"; exit 1; }
|
|
46
|
+
# clubedge:end
|
|
36
47
|
- run: pnpm test
|
|
37
48
|
# Build the way the app is developed: with the env example copied into place.
|
|
38
49
|
- run: cp .env.example apps/web/.env.local
|
package/template/README.md
CHANGED
|
@@ -57,6 +57,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
|
|
|
57
57
|
|
|
58
58
|
- Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
|
|
59
59
|
- Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth -->
|
|
60
|
+
- Firebase Authentication through the app's own pages, with server-side session cookies, a protected dashboard, and rate-limited sign-in; the Auth Emulator in development. <!-- clubedge:only auth=firebase -->
|
|
61
|
+
- Appwrite accounts through the app's own pages, on Appwrite Cloud or your own server, with server-side session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=appwrite -->
|
|
60
62
|
- Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
|
|
61
63
|
- Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
|
|
62
64
|
- An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none -->
|
|
@@ -97,7 +99,7 @@ Pick modules when you create a project, and change them later with `pnpm clubedg
|
|
|
97
99
|
| Module | Options | Default |
|
|
98
100
|
| ------------- | --------------------------------------------------------- | ---------- |
|
|
99
101
|
| Framework | `next`, `tanstack-start` | `next` |
|
|
100
|
-
| Auth | `supabase`, `better-auth`, `none`
|
|
102
|
+
| Auth | `supabase`, `better-auth`, `firebase`, `appwrite`, `none` | `supabase` |
|
|
101
103
|
| Storage | `s3`, `supabase`, `none` | `s3` |
|
|
102
104
|
| Cache | `redis`, `memory` | `redis` |
|
|
103
105
|
| Email | `smtp`, `resend`, `none` | `smtp` |
|
|
@@ -219,6 +221,12 @@ flowchart TB
|
|
|
219
221
|
App -->|auth interface and server sessions| Auth[Better Auth]
|
|
220
222
|
Auth -->|users and sessions| DB
|
|
221
223
|
%% clubedge:end
|
|
224
|
+
%% clubedge:if auth=firebase
|
|
225
|
+
App -->|auth interface and server sessions| Auth[Firebase Authentication]
|
|
226
|
+
%% clubedge:end
|
|
227
|
+
%% clubedge:if auth=appwrite
|
|
228
|
+
App -->|auth interface and server sessions| Auth[Appwrite]
|
|
229
|
+
%% clubedge:end
|
|
222
230
|
%% clubedge:if cache=redis
|
|
223
231
|
App -->|cache and rate limits| Redis[(Redis, or memory)]
|
|
224
232
|
%% clubedge:end
|
|
@@ -348,6 +356,8 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
|
|
|
348
356
|
- `packages/auth/`: `AuthProvider` and `CookieStore` interfaces <!-- clubedge:only auth!=none -->
|
|
349
357
|
- `packages/auth-supabase/`: Supabase Auth adapter <!-- clubedge:only auth=supabase -->
|
|
350
358
|
- `packages/auth-better-auth/`: Better Auth adapter, with its own tables and migrations <!-- clubedge:only auth=better-auth -->
|
|
359
|
+
- `packages/auth-firebase/`: Firebase Authentication adapter, with session cookies <!-- clubedge:only auth=firebase -->
|
|
360
|
+
- `packages/auth-appwrite/`: Appwrite adapter, with session cookies <!-- clubedge:only auth=appwrite -->
|
|
351
361
|
- `packages/storage/`: `StorageProvider` interface <!-- clubedge:only storage!=none -->
|
|
352
362
|
- `packages/storage-s3/`: S3-compatible storage adapter <!-- clubedge:only storage=s3 -->
|
|
353
363
|
- `packages/storage-supabase/`: Supabase Storage adapter <!-- clubedge:only storage=supabase -->
|
package/template/SETUP.md
CHANGED
|
@@ -43,6 +43,8 @@ The variables are:
|
|
|
43
43
|
- `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=next && auth=supabase -->
|
|
44
44
|
- `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=tanstack-start && auth=supabase -->
|
|
45
45
|
- `BETTER_AUTH_SECRET` (for sign-in): see [Better Auth](#better-auth). <!-- clubedge:only auth=better-auth -->
|
|
46
|
+
- `FIREBASE_PROJECT_ID`, `FIREBASE_API_KEY`, and in production a service account: see [Firebase Authentication](#firebase-authentication). <!-- clubedge:only auth=firebase -->
|
|
47
|
+
- `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT_ID`, and `APPWRITE_API_KEY` (for sign-in): see [Appwrite](#appwrite). <!-- clubedge:only auth=appwrite -->
|
|
46
48
|
- `REDIS_URL` (optional): see [Redis](#redis). <!-- clubedge:only cache=redis -->
|
|
47
49
|
- `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
|
|
48
50
|
- `RESEND_API_KEY` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=resend -->
|
|
@@ -103,6 +105,41 @@ These emails are sent by the `send-email` [background job](#background-jobs), so
|
|
|
103
105
|
|
|
104
106
|
<!-- clubedge:end -->
|
|
105
107
|
|
|
108
|
+
<!-- clubedge:if auth=firebase -->
|
|
109
|
+
|
|
110
|
+
## Firebase Authentication
|
|
111
|
+
|
|
112
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Password reset and email confirmation links are generated by Firebase and sent through the [email module](#email), so they open the app's own pages and need no setup in the Firebase console. <!-- clubedge:only email!=none -->
|
|
113
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Without the email module there is no password reset. <!-- clubedge:only email=none -->
|
|
114
|
+
|
|
115
|
+
In development, `pnpm auth:dev` starts Firebase's Auth Emulator (`firebase.json`, port 9099), which the example environment points at: no Firebase project or account is needed. Accounts live in memory until it stops.
|
|
116
|
+
|
|
117
|
+
In production:
|
|
118
|
+
|
|
119
|
+
1. In the [Firebase console](https://console.firebase.google.com), create a project and turn on **Email/Password** under Authentication, Sign-in method.
|
|
120
|
+
2. Set `FIREBASE_PROJECT_ID` and `FIREBASE_API_KEY` (the Web API key, under Project settings, General).
|
|
121
|
+
3. Under Project settings, Service accounts, generate a private key, and set `FIREBASE_CLIENT_EMAIL` and `FIREBASE_PRIVATE_KEY` from it (the key's line breaks as `\n` are fine).
|
|
122
|
+
4. Remove `FIREBASE_AUTH_EMULATOR_HOST`; `pnpm check:env` refuses it in production.
|
|
123
|
+
|
|
124
|
+
Changing a password or resetting it signs out every other session; deleting an account removes it from Firebase after the app's own data for the user.
|
|
125
|
+
|
|
126
|
+
<!-- clubedge:end -->
|
|
127
|
+
|
|
128
|
+
<!-- clubedge:if auth=appwrite -->
|
|
129
|
+
|
|
130
|
+
## Appwrite
|
|
131
|
+
|
|
132
|
+
[Appwrite](https://appwrite.io) keeps the accounts, on Appwrite Cloud or your own server; the app's own pages sign users in, and the server holds the session's secret in an HTTP-only cookie (`a_session_<project>`). Appwrite sends the email confirmation and password recovery emails itself, through its SMTP settings, with links back to the app's `/auth/callback` and `/reset-password`.
|
|
133
|
+
|
|
134
|
+
1. Create a project in [Appwrite Cloud](https://cloud.appwrite.io) (free tier) or on your Appwrite server.
|
|
135
|
+
2. Under Overview, Platforms, add a **Web** platform for the app's hostname: `localhost` in development, your domain in production. Appwrite only sends links to registered hostnames.
|
|
136
|
+
3. Under Overview, Integrations, API keys, create a key with the `sessions.write` and `users.write` scopes.
|
|
137
|
+
4. Set `APPWRITE_ENDPOINT` (such as `https://cloud.appwrite.io/v1`), `APPWRITE_PROJECT_ID`, and `APPWRITE_API_KEY`. The key stays on the server.
|
|
138
|
+
|
|
139
|
+
Changing or resetting a password signs out every other session; deleting an account removes the Appwrite user after the app's own data for them. A self-hosted Appwrite sends email only once its SMTP settings (`_APP_SMTP_HOST` and the rest) are set.
|
|
140
|
+
|
|
141
|
+
<!-- clubedge:end -->
|
|
142
|
+
|
|
106
143
|
## Local services
|
|
107
144
|
|
|
108
145
|
<!-- clubedge:if infra=docker -->
|
|
@@ -37,6 +37,27 @@ BETTER_AUTH_SECRET=
|
|
|
37
37
|
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
38
38
|
# clubedge:end
|
|
39
39
|
|
|
40
|
+
# clubedge:if auth=firebase
|
|
41
|
+
# Firebase Authentication. In development, pnpm auth:dev starts the Auth Emulator, which these
|
|
42
|
+
# values point at. In production, set your project's id and Web API key (Project settings,
|
|
43
|
+
# General) and a service account (Project settings, Service accounts), and remove
|
|
44
|
+
# FIREBASE_AUTH_EMULATOR_HOST.
|
|
45
|
+
FIREBASE_PROJECT_ID=demo-clubedge
|
|
46
|
+
FIREBASE_API_KEY=demo-api-key
|
|
47
|
+
FIREBASE_CLIENT_EMAIL=
|
|
48
|
+
FIREBASE_PRIVATE_KEY=
|
|
49
|
+
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
|
|
50
|
+
# clubedge:end
|
|
51
|
+
|
|
52
|
+
# clubedge:if auth=appwrite
|
|
53
|
+
# Appwrite (https://appwrite.io), on Appwrite Cloud or your own server. Create a project, add a
|
|
54
|
+
# Web platform for this app's hostname (localhost in development), and an API key with the
|
|
55
|
+
# sessions.write and users.write scopes. Appwrite sends its own emails (its SMTP settings).
|
|
56
|
+
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
|
57
|
+
APPWRITE_PROJECT_ID=
|
|
58
|
+
APPWRITE_API_KEY=
|
|
59
|
+
# clubedge:end
|
|
60
|
+
|
|
40
61
|
# clubedge:if cache=redis
|
|
41
62
|
# Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
|
|
42
63
|
# process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@clubedge/auth": "workspace:*",
|
|
17
|
+
"@clubedge/auth-appwrite": "workspace:*",
|
|
17
18
|
"@clubedge/auth-better-auth": "workspace:*",
|
|
19
|
+
"@clubedge/auth-firebase": "workspace:*",
|
|
18
20
|
"@clubedge/auth-supabase": "workspace:*",
|
|
19
21
|
"@clubedge/cache": "workspace:*",
|
|
20
22
|
"@clubedge/cache-redis": "workspace:*",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { optional, parseEnv } from "./server";
|
|
3
|
-
// clubedge:if auth=better-auth
|
|
3
|
+
// clubedge:if auth=better-auth|firebase|appwrite
|
|
4
4
|
import { getAppUrl } from "./server";
|
|
5
5
|
// clubedge:end
|
|
6
6
|
|
|
@@ -60,3 +60,68 @@ export function getBetterAuthEnv(): BetterAuthEnv | null {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
// clubedge:end
|
|
63
|
+
|
|
64
|
+
// clubedge:if auth=firebase
|
|
65
|
+
const firebaseAuthSchema = z.object({
|
|
66
|
+
FIREBASE_PROJECT_ID: optional(z.string()),
|
|
67
|
+
// The project's Web API key: Project settings, General.
|
|
68
|
+
FIREBASE_API_KEY: optional(z.string()),
|
|
69
|
+
// A service account: Project settings, Service accounts. Not needed with the emulator.
|
|
70
|
+
FIREBASE_CLIENT_EMAIL: optional(z.string()),
|
|
71
|
+
FIREBASE_PRIVATE_KEY: optional(z.string()),
|
|
72
|
+
// The Auth Emulator (pnpm auth:dev) in development, such as 127.0.0.1:9099.
|
|
73
|
+
FIREBASE_AUTH_EMULATOR_HOST: optional(z.string()),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export interface FirebaseAuthEnv {
|
|
77
|
+
projectId: string;
|
|
78
|
+
apiKey: string;
|
|
79
|
+
clientEmail?: string;
|
|
80
|
+
privateKey?: string;
|
|
81
|
+
emulatorHost?: string;
|
|
82
|
+
baseUrl: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Firebase Auth settings, or null until the project id and Web API key are set. */
|
|
86
|
+
export function getFirebaseAuthEnv(): FirebaseAuthEnv | null {
|
|
87
|
+
const env = parseEnv(firebaseAuthSchema, "Firebase Auth");
|
|
88
|
+
if (!env.FIREBASE_PROJECT_ID || !env.FIREBASE_API_KEY) return null;
|
|
89
|
+
return {
|
|
90
|
+
projectId: env.FIREBASE_PROJECT_ID,
|
|
91
|
+
apiKey: env.FIREBASE_API_KEY,
|
|
92
|
+
...(env.FIREBASE_CLIENT_EMAIL ? { clientEmail: env.FIREBASE_CLIENT_EMAIL } : {}),
|
|
93
|
+
...(env.FIREBASE_PRIVATE_KEY ? { privateKey: env.FIREBASE_PRIVATE_KEY } : {}),
|
|
94
|
+
...(env.FIREBASE_AUTH_EMULATOR_HOST ? { emulatorHost: env.FIREBASE_AUTH_EMULATOR_HOST } : {}),
|
|
95
|
+
baseUrl: getAppUrl(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// clubedge:end
|
|
99
|
+
|
|
100
|
+
// clubedge:if auth=appwrite
|
|
101
|
+
const appwriteAuthSchema = z.object({
|
|
102
|
+
// The API endpoint, such as https://cloud.appwrite.io/v1 or your server's /v1.
|
|
103
|
+
APPWRITE_ENDPOINT: optional(z.url()),
|
|
104
|
+
APPWRITE_PROJECT_ID: optional(z.string()),
|
|
105
|
+
// An API key with the sessions.write and users.write scopes (Overview, Integrations, API keys).
|
|
106
|
+
APPWRITE_API_KEY: optional(z.string()),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export interface AppwriteAuthEnv {
|
|
110
|
+
endpoint: string;
|
|
111
|
+
projectId: string;
|
|
112
|
+
apiKey: string;
|
|
113
|
+
baseUrl: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Appwrite settings, or null until the endpoint, project, and API key are set. */
|
|
117
|
+
export function getAppwriteAuthEnv(): AppwriteAuthEnv | null {
|
|
118
|
+
const env = parseEnv(appwriteAuthSchema, "Appwrite");
|
|
119
|
+
if (!env.APPWRITE_ENDPOINT || !env.APPWRITE_PROJECT_ID || !env.APPWRITE_API_KEY) return null;
|
|
120
|
+
return {
|
|
121
|
+
endpoint: env.APPWRITE_ENDPOINT,
|
|
122
|
+
projectId: env.APPWRITE_PROJECT_ID,
|
|
123
|
+
apiKey: env.APPWRITE_API_KEY,
|
|
124
|
+
baseUrl: getAppUrl(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// clubedge:end
|
|
@@ -23,6 +23,17 @@ export function requiredVariables(env: Env): Requirement[] {
|
|
|
23
23
|
// clubedge:if auth=better-auth
|
|
24
24
|
{ key: "BETTER_AUTH_SECRET", use: "Better Auth sessions" },
|
|
25
25
|
// clubedge:end
|
|
26
|
+
// clubedge:if auth=appwrite
|
|
27
|
+
{ key: "APPWRITE_ENDPOINT", use: "Appwrite", url: true },
|
|
28
|
+
{ key: "APPWRITE_PROJECT_ID", use: "Appwrite" },
|
|
29
|
+
{ key: "APPWRITE_API_KEY", use: "Appwrite" },
|
|
30
|
+
// clubedge:end
|
|
31
|
+
// clubedge:if auth=firebase
|
|
32
|
+
{ key: "FIREBASE_PROJECT_ID", use: "Firebase Authentication" },
|
|
33
|
+
{ key: "FIREBASE_API_KEY", use: "Firebase Authentication" },
|
|
34
|
+
{ key: "FIREBASE_CLIENT_EMAIL", use: "Firebase Authentication (the service account)" },
|
|
35
|
+
{ key: "FIREBASE_PRIVATE_KEY", use: "Firebase Authentication (the service account)" },
|
|
36
|
+
// clubedge:end
|
|
26
37
|
// clubedge:if cache=redis
|
|
27
38
|
{ key: "REDIS_URL", use: "the cache and rate limits", url: true },
|
|
28
39
|
// clubedge:end
|
|
@@ -105,6 +116,14 @@ export function checkProductionEnv(env: Env): ProductionEnvCheck {
|
|
|
105
116
|
invalid.push("INNGEST_DEV is set: production uses Inngest Cloud's keys, so remove it");
|
|
106
117
|
}
|
|
107
118
|
// clubedge:end
|
|
119
|
+
// clubedge:if auth=firebase
|
|
120
|
+
// The emulator only runs on a developer's machine.
|
|
121
|
+
if (env.FIREBASE_AUTH_EMULATOR_HOST?.trim()) {
|
|
122
|
+
invalid.push(
|
|
123
|
+
"FIREBASE_AUTH_EMULATOR_HOST is set: production uses your Firebase project, so remove it",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
// clubedge:end
|
|
108
127
|
// clubedge:if auth=better-auth
|
|
109
128
|
const secret = env.BETTER_AUTH_SECRET?.trim();
|
|
110
129
|
if (secret && secret.length < 32) invalid.push("BETTER_AUTH_SECRET needs at least 32 characters");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CircleAlert } from "lucide-react";
|
|
2
|
+
import { linkToken } from "@clubedge/auth";
|
|
2
3
|
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
3
4
|
|
|
4
5
|
import { AuthForm, PasswordField, SubmitButton } from "@/components/auth/auth-form-controls";
|
|
@@ -15,7 +16,8 @@ const asString = (value: unknown) => (typeof value === "string" ? value : undefi
|
|
|
15
16
|
export const Route = createFileRoute("/reset-password")({
|
|
16
17
|
validateSearch: (search: Record<string, unknown>): { error?: string; token?: string } => ({
|
|
17
18
|
error: asString(search.error),
|
|
18
|
-
token
|
|
19
|
+
// Appwrite's links carry userId and secret instead of a token.
|
|
20
|
+
token: linkToken(search, "token") ?? undefined,
|
|
19
21
|
}),
|
|
20
22
|
component: ResetPasswordPage,
|
|
21
23
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@tanstack/react-start/server-only";
|
|
2
|
-
import type { AuthEmailSender } from "@clubedge/auth
|
|
2
|
+
import type { AuthEmailSender } from "@clubedge/auth";
|
|
3
3
|
// clubedge:if email!=none && jobs=none
|
|
4
4
|
import { email } from "@/server/email";
|
|
5
5
|
// clubedge:end
|
|
@@ -9,7 +9,7 @@ import { getJobs } from "@/server/jobs";
|
|
|
9
9
|
// clubedge:end
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* How
|
|
12
|
+
* How the auth provider sends verification and password reset emails (Better Auth, Firebase): through the email module, or
|
|
13
13
|
* not at all without one, which turns both features off.
|
|
14
14
|
*/
|
|
15
15
|
export function authEmailSender(): AuthEmailSender | undefined {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "@tanstack/react-start/server-only";
|
|
2
|
+
import { linkToken } from "@clubedge/auth";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getClientIp, safeRedirectPath, reportError } from "@clubedge/core";
|
|
4
5
|
import { getAppUrl } from "@/env/server";
|
|
@@ -159,7 +160,8 @@ export async function handleSignOut(): Promise<Response> {
|
|
|
159
160
|
|
|
160
161
|
export async function handleAuthCallback(request: Request): Promise<Response> {
|
|
161
162
|
const url = new URL(request.url);
|
|
162
|
-
|
|
163
|
+
// Appwrite's links carry userId and secret instead of a code.
|
|
164
|
+
const code = linkToken(url.searchParams, "code");
|
|
163
165
|
const next = safeRedirectPath(url.searchParams.get("next"));
|
|
164
166
|
|
|
165
167
|
if (code && isAuthConfigured()) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "@tanstack/react-start/server-only";
|
|
2
|
+
import type { AuthProvider, AuthUser } from "@clubedge/auth";
|
|
3
|
+
import { createAppwriteAuth } from "@clubedge/auth-appwrite";
|
|
4
|
+
import { AppError, reportError } from "@clubedge/core";
|
|
5
|
+
import { getAppwriteAuthEnv } from "@/env/auth";
|
|
6
|
+
import { requestCookieStore } from "@/server/cookies";
|
|
7
|
+
|
|
8
|
+
export type { AuthUser } from "@clubedge/auth";
|
|
9
|
+
|
|
10
|
+
export function isAuthConfigured(): boolean {
|
|
11
|
+
return getAppwriteAuthEnv() !== null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Request-scoped auth provider. Throws when authentication is not configured. */
|
|
15
|
+
export function getAuth(): AuthProvider {
|
|
16
|
+
const env = getAppwriteAuthEnv();
|
|
17
|
+
if (!env) throw new Error("Appwrite is not configured.");
|
|
18
|
+
return createAppwriteAuth({ ...env, cookies: requestCookieStore() });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Validates the session. Runs once per request from src/start.ts. */
|
|
22
|
+
export async function refreshAuthSession(): Promise<void> {
|
|
23
|
+
if (!isAuthConfigured()) return;
|
|
24
|
+
try {
|
|
25
|
+
await getAuth().refreshSession();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Pages still render; protected handlers re-check the user and reject when needed.
|
|
28
|
+
reportError(error, { message: "Session refresh failed" });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getCurrentUser(): Promise<AuthUser | null> {
|
|
33
|
+
if (!isAuthConfigured()) return null;
|
|
34
|
+
return getAuth().getUser();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** For server functions and routes: throws a 401 AppError when signed out. */
|
|
38
|
+
export async function requireUser(): Promise<AuthUser> {
|
|
39
|
+
const user = await getCurrentUser();
|
|
40
|
+
if (!user) throw new AppError("Authentication required.", "UNAUTHENTICATED", 401);
|
|
41
|
+
return user;
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import "@tanstack/react-start/server-only";
|
|
2
|
+
import type { AuthProvider, AuthUser } from "@clubedge/auth";
|
|
3
|
+
import { createFirebaseAuth } from "@clubedge/auth-firebase";
|
|
4
|
+
import { siteConfig } from "@/config/site";
|
|
5
|
+
import { authEmailSender } from "@/server/auth-email";
|
|
6
|
+
import { AppError, reportError } from "@clubedge/core";
|
|
7
|
+
import { getFirebaseAuthEnv } from "@/env/auth";
|
|
8
|
+
import { requestCookieStore } from "@/server/cookies";
|
|
9
|
+
|
|
10
|
+
export type { AuthUser } from "@clubedge/auth";
|
|
11
|
+
|
|
12
|
+
export function isAuthConfigured(): boolean {
|
|
13
|
+
return getFirebaseAuthEnv() !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Request-scoped auth provider. Throws when authentication is not configured. */
|
|
17
|
+
export function getAuth(): AuthProvider {
|
|
18
|
+
const env = getFirebaseAuthEnv();
|
|
19
|
+
if (!env) throw new Error("Firebase Auth is not configured.");
|
|
20
|
+
return createFirebaseAuth({
|
|
21
|
+
...env,
|
|
22
|
+
cookies: requestCookieStore(),
|
|
23
|
+
sendEmail: authEmailSender(),
|
|
24
|
+
appName: siteConfig.name,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Validates the session. Runs once per request from src/start.ts. */
|
|
29
|
+
export async function refreshAuthSession(): Promise<void> {
|
|
30
|
+
if (!isAuthConfigured()) return;
|
|
31
|
+
try {
|
|
32
|
+
await getAuth().refreshSession();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Pages still render; protected handlers re-check the user and reject when needed.
|
|
35
|
+
reportError(error, { message: "Session refresh failed" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getCurrentUser(): Promise<AuthUser | null> {
|
|
40
|
+
if (!isAuthConfigured()) return null;
|
|
41
|
+
return getAuth().getUser();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** For server functions and routes: throws a 401 AppError when signed out. */
|
|
45
|
+
export async function requireUser(): Promise<AuthUser> {
|
|
46
|
+
const user = await getCurrentUser();
|
|
47
|
+
if (!user) throw new AppError("Authentication required.", "UNAUTHENTICATED", 401);
|
|
48
|
+
return user;
|
|
49
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@clubedge/auth": "workspace:*",
|
|
16
|
+
"@clubedge/auth-appwrite": "workspace:*",
|
|
16
17
|
"@clubedge/auth-better-auth": "workspace:*",
|
|
18
|
+
"@clubedge/auth-firebase": "workspace:*",
|
|
17
19
|
"@clubedge/auth-supabase": "workspace:*",
|
|
18
20
|
"@clubedge/cache": "workspace:*",
|
|
19
21
|
"@clubedge/cache-redis": "workspace:*",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
import { linkToken } from "@clubedge/auth";
|
|
2
3
|
import { safeRedirectPath } from "@clubedge/core";
|
|
3
4
|
import { loginUrl } from "@/lib/login-url";
|
|
4
5
|
import { getAuth } from "@/server/auth";
|
|
@@ -14,7 +15,8 @@ function redirectTo(path: string) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function GET(request: NextRequest) {
|
|
17
|
-
|
|
18
|
+
// Appwrite's links carry userId and secret instead of a code.
|
|
19
|
+
const code = linkToken(request.nextUrl.searchParams, "code");
|
|
18
20
|
const next = safeRedirectPath(request.nextUrl.searchParams.get("next"));
|
|
19
21
|
|
|
20
22
|
if (code) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CircleAlert } from "lucide-react";
|
|
2
|
+
import { linkToken } from "@clubedge/auth";
|
|
2
3
|
import Link from "next/link";
|
|
3
4
|
|
|
4
5
|
import { resetPassword } from "@/app/actions/auth";
|
|
@@ -14,9 +15,12 @@ const errorMessages: Record<string, string> = {
|
|
|
14
15
|
export default async function ResetPasswordPage({
|
|
15
16
|
searchParams,
|
|
16
17
|
}: {
|
|
17
|
-
searchParams: Promise<{ error?: string; token?: string }>;
|
|
18
|
+
searchParams: Promise<{ error?: string; token?: string; userId?: string; secret?: string }>;
|
|
18
19
|
}) {
|
|
19
|
-
const
|
|
20
|
+
const search = await searchParams;
|
|
21
|
+
const { error } = search;
|
|
22
|
+
// Appwrite's links carry userId and secret instead of a token.
|
|
23
|
+
const token = linkToken(search, "token") ?? undefined;
|
|
20
24
|
|
|
21
25
|
return (
|
|
22
26
|
<AuthPage
|