@percepta/create 3.1.4 → 3.2.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/README.md +8 -8
- package/dist/git-ops-C2CIjuce.js +51 -0
- package/dist/git-ops-C2CIjuce.js.map +1 -0
- package/dist/index.js +1085 -1072
- package/dist/index.js.map +1 -0
- package/dist/init-CtCp7Tv2.js +52 -0
- package/dist/init-CtCp7Tv2.js.map +1 -0
- package/dist/status-CKe4aKso.js +48 -0
- package/dist/status-CKe4aKso.js.map +1 -0
- package/dist/sync-D1vkoofl.js +101 -0
- package/dist/sync-D1vkoofl.js.map +1 -0
- package/dist/upstream-D-LH_1z4.js +85 -0
- package/dist/upstream-D-LH_1z4.js.map +1 -0
- package/package.json +23 -24
- package/template-versions.json +1 -1
- package/templates/monorepo/.github/workflows/access-control.yml +38 -0
- package/templates/monorepo/README.md +41 -2
- package/templates/monorepo/access/README.md +39 -0
- package/templates/monorepo/access/bootstrap-grants.yaml.example +9 -0
- package/templates/monorepo/access/dev-grants.yaml.example +19 -0
- package/templates/monorepo/access/dev-groups.yaml.example +8 -0
- package/templates/monorepo/access/reconcile.yaml.example +11 -0
- package/templates/monorepo/auth/README.md +26 -0
- package/templates/monorepo/auth/drizzle.config.ts +13 -0
- package/templates/monorepo/auth/package.json +32 -0
- package/templates/monorepo/auth/scripts/setup-database.ts +57 -0
- package/templates/monorepo/auth/src/auth.ts +77 -0
- package/templates/monorepo/auth/src/config/database.ts +31 -0
- package/templates/monorepo/auth/src/drizzle/db.ts +9 -0
- package/templates/monorepo/auth/src/drizzle/migrations/0000_shared_auth.sql +89 -0
- package/templates/monorepo/auth/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/accounts.ts +1 -6
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/sessions.ts +1 -5
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/verifications.ts +0 -4
- package/templates/monorepo/auth/src/drizzle/schema/groups.ts +16 -0
- package/templates/monorepo/auth/src/drizzle/schema/index.ts +5 -0
- package/templates/monorepo/auth/src/drizzle/schema/users.ts +6 -0
- package/templates/monorepo/auth/src/index.ts +1 -0
- package/templates/monorepo/auth/src/scim/README.md +6 -0
- package/templates/monorepo/auth/tsconfig.json +12 -0
- package/templates/monorepo/package.json.template +18 -6
- package/templates/monorepo/pnpm-workspace.yaml +1 -0
- package/templates/webapp/AGENTS.md +13 -6
- package/templates/webapp/README.md +34 -18
- package/templates/webapp/agent-skills/access-control.md +301 -0
- package/templates/webapp/agent-skills/database.md +1 -1
- package/templates/webapp/docker-compose.yml +16 -0
- package/templates/webapp/env.example.template +9 -0
- package/templates/webapp/next.config.ts +1 -0
- package/templates/webapp/package.json.template +8 -4
- package/templates/webapp/scripts/seed.ts +87 -36
- package/templates/webapp/scripts/setup-database.ts +7 -1
- package/templates/webapp/scripts/start.sh +0 -9
- package/templates/webapp/src/access/access.manifest.ts +15 -0
- package/templates/webapp/src/access/schema.zed +7 -0
- package/templates/webapp/src/app/(app)/admin/_lib/PrincipalRoleTable.tsx +113 -0
- package/templates/webapp/src/app/(app)/admin/_lib/accessAdmin.ts +85 -0
- package/templates/webapp/src/app/(app)/admin/groups/page.tsx +117 -0
- package/templates/webapp/src/app/(app)/admin/users/page.tsx +79 -0
- package/templates/webapp/src/app/(app)/layout.tsx +16 -2
- package/templates/webapp/src/app/(app)/page.tsx +1 -12
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +2 -5
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +2 -5
- package/templates/webapp/src/config/getEnvConfig.ts +8 -0
- package/templates/webapp/src/drizzle/db.ts +3 -4
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +1 -57
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +1 -347
- package/templates/webapp/src/drizzle/schema/index.ts +3 -4
- package/templates/webapp/src/lib/auth/index.ts +6 -81
- package/templates/webapp/src/server/api/root.ts +4 -1
- package/templates/webapp/src/server/api/routers/access.ts +13 -0
- package/templates/webapp/src/server/trpc.ts +42 -8
- package/templates/webapp/src/services/DatabaseService.ts +4 -5
- package/templates/webapp/src/services/access/AppAccessControl.ts +39 -0
- package/dist/chunk-CO3YWUD6.js +0 -139
- package/dist/chunk-DCM7JOSC.js +0 -49
- package/dist/chunk-V5EJIUBJ.js +0 -60
- package/dist/index.d.ts +0 -1
- package/dist/init-EQZ2TCSJ.js +0 -96
- package/dist/status-QW5TQDYY.js +0 -76
- package/dist/sync-RLBZDOFB.js +0 -136
- package/dist/upstream-TQFVPMEG.js +0 -144
- package/templates/webapp/scripts/create-user.ts +0 -47
- package/templates/webapp/src/drizzle/schema/auth/users.ts +0 -38
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percepta/create",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Scaffold a new Mosaic package",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"create",
|
|
8
|
+
"mosaic",
|
|
9
|
+
"nextjs",
|
|
10
|
+
"percepta",
|
|
11
|
+
"scaffold",
|
|
12
|
+
"template"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
6
15
|
"bin": {
|
|
7
16
|
"create": "./dist/index.js"
|
|
8
17
|
},
|
|
@@ -11,6 +20,10 @@
|
|
|
11
20
|
"templates",
|
|
12
21
|
"template-versions.json"
|
|
13
22
|
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
14
27
|
"dependencies": {
|
|
15
28
|
"chalk": "^5.4.1",
|
|
16
29
|
"commander": "^13.1.0",
|
|
@@ -24,36 +37,22 @@
|
|
|
24
37
|
"@types/fs-extra": "^11.0.4",
|
|
25
38
|
"@types/node": "^24.1.0",
|
|
26
39
|
"@types/validate-npm-package-name": "^4.0.2",
|
|
27
|
-
"tsup": "^8.4.0",
|
|
28
|
-
"typescript": "^5.7.3",
|
|
29
40
|
"vitest": "^4.0.0",
|
|
30
|
-
"@percepta/build": "0.
|
|
41
|
+
"@percepta/build": "0.5.1"
|
|
31
42
|
},
|
|
32
43
|
"engines": {
|
|
33
44
|
"node": ">=18.0.0"
|
|
34
45
|
},
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public"
|
|
37
|
-
},
|
|
38
|
-
"keywords": [
|
|
39
|
-
"create",
|
|
40
|
-
"template",
|
|
41
|
-
"nextjs",
|
|
42
|
-
"mosaic",
|
|
43
|
-
"percepta",
|
|
44
|
-
"scaffold",
|
|
45
|
-
"cli"
|
|
46
|
-
],
|
|
47
|
-
"license": "MIT",
|
|
48
46
|
"scripts": {
|
|
49
|
-
"build": "
|
|
50
|
-
"
|
|
51
|
-
"
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"dev": "tsdown --watch",
|
|
49
|
+
"clean": "rimraf dist",
|
|
52
50
|
"typecheck": "tsc --noEmit",
|
|
53
|
-
"sync-template": "tsx scripts/sync-template.ts",
|
|
54
|
-
"template:tag": "tsx scripts/template-tag.ts",
|
|
55
51
|
"test": "vitest run",
|
|
56
52
|
"test:watch": "vitest",
|
|
57
|
-
"test:template": "bash scripts/test-template.sh"
|
|
53
|
+
"test:template": "bash scripts/test-template.sh",
|
|
54
|
+
"create:local": "pnpm build && node dist/index.js",
|
|
55
|
+
"sync-template": "tsx scripts/sync-template.ts",
|
|
56
|
+
"template:tag": "tsx scripts/template-tag.ts"
|
|
58
57
|
}
|
|
59
58
|
}
|
package/template-versions.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Access Control
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request: {}
|
|
5
|
+
|
|
6
|
+
env:
|
|
7
|
+
PNPM_VERSION: 10.x
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
access-control:
|
|
11
|
+
name: Merge and Validate Access Schema
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout repository
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup PNPM
|
|
19
|
+
uses: pnpm/action-setup@v4
|
|
20
|
+
with:
|
|
21
|
+
version: ${{ env.PNPM_VERSION }}
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: 22
|
|
27
|
+
cache: pnpm
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: pnpm install --frozen-lockfile
|
|
31
|
+
|
|
32
|
+
- name: Merge and validate access schema
|
|
33
|
+
run: |
|
|
34
|
+
if find packages -path '*/src/access/access.manifest.ts' | grep -q .; then
|
|
35
|
+
pnpm access:validate
|
|
36
|
+
else
|
|
37
|
+
echo "No app access manifests found yet; skipping access schema validation."
|
|
38
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# __APP_TITLE__
|
|
2
2
|
|
|
3
|
-
A monorepo powered by [pnpm workspaces](https://pnpm.io/workspaces).
|
|
3
|
+
A customer monorepo powered by [pnpm workspaces](https://pnpm.io/workspaces).
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
@@ -11,6 +11,12 @@ pnpm install
|
|
|
11
11
|
# Run development mode for all packages
|
|
12
12
|
pnpm dev
|
|
13
13
|
|
|
14
|
+
# Set up local services, access-control topology, databases, and seed users
|
|
15
|
+
pnpm run setup
|
|
16
|
+
|
|
17
|
+
# Merge and validate customer access-control schema
|
|
18
|
+
pnpm access:validate
|
|
19
|
+
|
|
14
20
|
# Build all packages
|
|
15
21
|
pnpm build
|
|
16
22
|
|
|
@@ -24,10 +30,43 @@ pnpm lint
|
|
|
24
30
|
## Structure
|
|
25
31
|
|
|
26
32
|
```
|
|
33
|
+
access/ # Customer-level SpiceDB fixtures and generated merge artifacts
|
|
34
|
+
auth/ # Shared Better Auth users/groups package for this customer
|
|
27
35
|
packages/
|
|
28
|
-
└── your-package/
|
|
36
|
+
└── your-package/ # Application and library packages
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Access Control
|
|
40
|
+
|
|
41
|
+
Application builders define app-local Zed schemas in each package's
|
|
42
|
+
`src/access/` directory. The root access scripts merge those schemas with the
|
|
43
|
+
shared `core/*` schema and apply customer-owned grants such
|
|
44
|
+
as application owners, application members, and bootstrap customer admins.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm access:merge
|
|
48
|
+
pnpm access:validate
|
|
49
|
+
pnpm access:apply
|
|
29
50
|
```
|
|
30
51
|
|
|
52
|
+
PR CI merges the customer schema and runs static schema/manifest validation.
|
|
53
|
+
`access:apply` is reserved for trusted deploy jobs and should run once per
|
|
54
|
+
target environment with that environment's SpiceDB credentials.
|
|
55
|
+
|
|
56
|
+
For local development, copy the example fixture files in `access/`, fill in
|
|
57
|
+
customer-global user/group IDs from `auth/`, then run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm access:seed-grants
|
|
61
|
+
pnpm access:bootstrap-customer-admin -- --subject core/user:<user-id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Shared Auth
|
|
65
|
+
|
|
66
|
+
The `auth/` workspace owns the customer-global Better Auth schema, including
|
|
67
|
+
`users`, `groups`, and `group_members`. Apps should consume this shared
|
|
68
|
+
identity layer instead of creating app-local users or groups.
|
|
69
|
+
|
|
31
70
|
## Adding a new package
|
|
32
71
|
|
|
33
72
|
Create a new directory in `packages/` with its own `package.json`:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Customer Access Control
|
|
2
|
+
|
|
3
|
+
This directory owns the customer-level SpiceDB deployment surface:
|
|
4
|
+
|
|
5
|
+
- `pnpm access:merge` combines every app's `src/access/schema.zed` with the shared core schema.
|
|
6
|
+
- `pnpm access:validate` validates the merged schema and app manifests.
|
|
7
|
+
- `pnpm access:apply` writes the merged schema and stable application topology links to SpiceDB.
|
|
8
|
+
- `pnpm access:seed-grants` and `pnpm access:apply-bootstrap-grants` apply YAML fixture grants.
|
|
9
|
+
- `pnpm access:bootstrap-customer-admin` creates the first direct customer-admin grant.
|
|
10
|
+
- `pnpm access:reconcile` repairs SpiceDB from an explicit local projection.
|
|
11
|
+
|
|
12
|
+
The source of truth for app-specific permissions remains the app's authored Zed
|
|
13
|
+
file. This package owns only customer-level composition and customer-admin
|
|
14
|
+
bootstrap.
|
|
15
|
+
|
|
16
|
+
## Local Bootstrap
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm access:merge
|
|
20
|
+
pnpm access:validate
|
|
21
|
+
cp access/bootstrap-grants.yaml.example access/bootstrap-grants.yaml
|
|
22
|
+
pnpm access:apply-local
|
|
23
|
+
pnpm access:apply-bootstrap-grants -- --endpoint localhost:50051 --insecure --key dev-spicedb-token
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `core/user:<users.id>` and `core/group:<groups.id>#member` subjects. The IDs
|
|
27
|
+
come from the shared `auth/` package tables, not from per-app user tables.
|
|
28
|
+
|
|
29
|
+
## Production Promotion
|
|
30
|
+
|
|
31
|
+
Run `pnpm access:validate` in PR CI. Run `pnpm access:apply` only from trusted deploy jobs
|
|
32
|
+
with the target environment's SpiceDB credentials. Promote the same merged
|
|
33
|
+
schema artifact through environments before app code that depends on new
|
|
34
|
+
relations or permissions.
|
|
35
|
+
|
|
36
|
+
Use expand/contract for destructive changes: add the new shape, deploy
|
|
37
|
+
dual-write/dual-read code, backfill with idempotent relationship writes,
|
|
38
|
+
reconcile, then remove old relationships and schema definitions in a later
|
|
39
|
+
deploy.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Production bootstrap grants for first deploy / break-glass access.
|
|
2
|
+
# Copy to bootstrap-grants.yaml, replace IDs with auth.users.id values, then run:
|
|
3
|
+
# pnpm access:apply-bootstrap-grants
|
|
4
|
+
customerAdmins:
|
|
5
|
+
- userId: "00000000-0000-0000-0000-000000000000"
|
|
6
|
+
|
|
7
|
+
applications: []
|
|
8
|
+
appRoles: []
|
|
9
|
+
resourceRelations: []
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Local development grants. Copy to dev-grants.yaml and replace IDs with rows
|
|
2
|
+
# from the shared auth.users / auth.groups tables.
|
|
3
|
+
customerAdmins:
|
|
4
|
+
- userId: "00000000-0000-0000-0000-000000000000"
|
|
5
|
+
|
|
6
|
+
applications:
|
|
7
|
+
- appNamespace: "people_app"
|
|
8
|
+
owners:
|
|
9
|
+
- userId: "00000000-0000-0000-0000-000000000000"
|
|
10
|
+
members:
|
|
11
|
+
- groupId: "11111111-1111-1111-1111-111111111111"
|
|
12
|
+
|
|
13
|
+
appRoles:
|
|
14
|
+
- appNamespace: "people_app"
|
|
15
|
+
role: "admin"
|
|
16
|
+
subjects:
|
|
17
|
+
- groupId: "11111111-1111-1111-1111-111111111111"
|
|
18
|
+
|
|
19
|
+
resourceRelations: []
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Future fixture source for the auth-owned groupSync adapter.
|
|
2
|
+
# SCIM/JIT is the production source of truth; this file is only for local dev.
|
|
3
|
+
groups:
|
|
4
|
+
- externalId: "dev-group-admins"
|
|
5
|
+
name: "Development Admins"
|
|
6
|
+
source: "fixture"
|
|
7
|
+
members:
|
|
8
|
+
- userExternalId: "dev-admin"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Explicit local projection for access:reconcile.
|
|
2
|
+
applications:
|
|
3
|
+
- appNamespace: "people_app"
|
|
4
|
+
|
|
5
|
+
groupMemberships:
|
|
6
|
+
- groupId: "11111111-1111-1111-1111-111111111111"
|
|
7
|
+
userIds:
|
|
8
|
+
- "00000000-0000-0000-0000-000000000000"
|
|
9
|
+
|
|
10
|
+
deletedUserIds: []
|
|
11
|
+
deletedGroupIds: []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Shared Auth
|
|
2
|
+
|
|
3
|
+
This workspace owns the customer-global Better Auth database schema. Apps in the
|
|
4
|
+
customer monorepo should import this package for session validation and user /
|
|
5
|
+
group table references instead of creating app-local auth tables.
|
|
6
|
+
|
|
7
|
+
Import auth as `@__APP_NAME__/auth`, the database handle as
|
|
8
|
+
`@__APP_NAME__/auth/db`, and table definitions as `@__APP_NAME__/auth/schema`
|
|
9
|
+
from app packages.
|
|
10
|
+
|
|
11
|
+
The important identity invariant is:
|
|
12
|
+
|
|
13
|
+
- `core/user:<id>` uses `users.id` from this package.
|
|
14
|
+
- `core/group:<id>#member` uses `groups.id` from this package.
|
|
15
|
+
- `group_members` is the local projection that reconcile uses to repair
|
|
16
|
+
SpiceDB group membership relationships.
|
|
17
|
+
|
|
18
|
+
SCIM/JIT protocol handlers are intentionally not implemented here yet; they
|
|
19
|
+
should feed the access-control `groupSync` ingestion contract when that adapter
|
|
20
|
+
lands.
|
|
21
|
+
|
|
22
|
+
## Database
|
|
23
|
+
|
|
24
|
+
By default this package uses the customer monorepo database name generated at
|
|
25
|
+
scaffold time. Override with `AUTH_DATABASE_NAME` or `AUTH_DATABASE_URL` when
|
|
26
|
+
the shared auth database lives somewhere else.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Config } from "drizzle-kit";
|
|
2
|
+
import { getAuthDatabaseConnectionString } from "./src/config/database";
|
|
3
|
+
|
|
4
|
+
const config: Config = {
|
|
5
|
+
schema: "./src/drizzle/schema",
|
|
6
|
+
out: "./src/drizzle/migrations",
|
|
7
|
+
dialect: "postgresql",
|
|
8
|
+
dbCredentials: {
|
|
9
|
+
url: getAuthDatabaseConnectionString(),
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default config;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME__/auth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Shared customer identity package.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./db": "./src/drizzle/db.ts",
|
|
10
|
+
"./schema": "./src/drizzle/schema/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"db:generate": "drizzle-kit generate",
|
|
15
|
+
"db:migrate": "drizzle-kit migrate",
|
|
16
|
+
"db:setup": "tsx ./scripts/setup-database.ts",
|
|
17
|
+
"db:setup-and-migrate": "pnpm db:setup && pnpm db:migrate"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@percepta/access-control": "0.3.2",
|
|
21
|
+
"better-auth": "^1.6.4",
|
|
22
|
+
"drizzle-orm": "^0.45.2",
|
|
23
|
+
"pg": "^8.16.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.1.0",
|
|
27
|
+
"@types/pg": "^8.15.4",
|
|
28
|
+
"drizzle-kit": "^0.31.4",
|
|
29
|
+
"tsx": "^4.20.3",
|
|
30
|
+
"typescript": "^5.8.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import { getAuthDatabaseConfig } from "../src/config/database";
|
|
3
|
+
|
|
4
|
+
const SHARED_DATABASES = new Set(["demos", "internal_apps"]);
|
|
5
|
+
|
|
6
|
+
async function main(): Promise<void> {
|
|
7
|
+
const { database, host, password, port, url, user } = getAuthDatabaseConfig();
|
|
8
|
+
|
|
9
|
+
if (url != null && url.length > 0) {
|
|
10
|
+
console.log("AUTH_DATABASE_URL is set; skipping CREATE DATABASE.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(`Setting up shared auth database: ${database}`);
|
|
15
|
+
console.log(`Host: ${host}:${port}`);
|
|
16
|
+
console.log(`User: ${user}`);
|
|
17
|
+
|
|
18
|
+
if (SHARED_DATABASES.has(database)) {
|
|
19
|
+
console.log(`${database} is a shared infra-managed database; skipping.`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const adminClient = new Pool({
|
|
24
|
+
database: "postgres",
|
|
25
|
+
host,
|
|
26
|
+
password,
|
|
27
|
+
port,
|
|
28
|
+
user,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await adminClient.query(
|
|
33
|
+
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
34
|
+
[database],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (result.rows.length === 0) {
|
|
38
|
+
await adminClient.query(
|
|
39
|
+
`CREATE DATABASE ${escapePgIdentifier(database)}`,
|
|
40
|
+
);
|
|
41
|
+
console.log(`Database ${database} created successfully.`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`Database ${database} already exists.`);
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
await adminClient.end();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function escapePgIdentifier(identifier: string): string {
|
|
51
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void main().catch((error) => {
|
|
55
|
+
console.error("Shared auth database setup failed:", error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { admin } from "better-auth/plugins";
|
|
4
|
+
import { db } from "./drizzle/db";
|
|
5
|
+
import { accounts } from "./drizzle/schema/auth/accounts";
|
|
6
|
+
import { sessions } from "./drizzle/schema/auth/sessions";
|
|
7
|
+
import { verifications } from "./drizzle/schema/auth/verifications";
|
|
8
|
+
import { users } from "./drizzle/schema/users";
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line n/no-process-env -- detecting Next.js build phase
|
|
11
|
+
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
|
12
|
+
|
|
13
|
+
function requiredEnv(name: string): string {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
if (value == null || value.length === 0) {
|
|
16
|
+
throw new Error(`${name} is required.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getSecret(): string {
|
|
22
|
+
if (isBuildPhase) {
|
|
23
|
+
return "build-placeholder-not-used-at-runtime";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return requiredEnv("BETTER_AUTH_SECRET");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createAuth() {
|
|
30
|
+
return betterAuth({
|
|
31
|
+
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
|
|
32
|
+
secret: getSecret(),
|
|
33
|
+
database: drizzleAdapter(db, {
|
|
34
|
+
provider: "pg",
|
|
35
|
+
schema: {
|
|
36
|
+
user: users,
|
|
37
|
+
session: sessions,
|
|
38
|
+
account: accounts,
|
|
39
|
+
verification: verifications,
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
emailAndPassword: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
},
|
|
45
|
+
plugins: [admin()],
|
|
46
|
+
advanced: {
|
|
47
|
+
database: {
|
|
48
|
+
generateId: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Auth = ReturnType<typeof createAuth>;
|
|
55
|
+
|
|
56
|
+
let authInstance: Auth | undefined;
|
|
57
|
+
|
|
58
|
+
function getAuth(): Auth {
|
|
59
|
+
authInstance ??= createAuth();
|
|
60
|
+
return authInstance;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Lazy proxy so app builds can import the shared auth package without requiring
|
|
65
|
+
* runtime secrets until Better Auth is actually used.
|
|
66
|
+
*/
|
|
67
|
+
export const auth: Auth = new Proxy({} as Auth, {
|
|
68
|
+
get(_target, prop, receiver) {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
70
|
+
return Reflect.get(getAuth(), prop, receiver);
|
|
71
|
+
},
|
|
72
|
+
has(_target, prop) {
|
|
73
|
+
return Reflect.has(getAuth(), prop);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type BetterAuthSession = typeof auth.$Infer.Session;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface AuthDatabaseConfig {
|
|
2
|
+
readonly database: string;
|
|
3
|
+
readonly host: string;
|
|
4
|
+
readonly password: string;
|
|
5
|
+
readonly port: number;
|
|
6
|
+
readonly url?: string;
|
|
7
|
+
readonly user: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getAuthDatabaseConfig(): AuthDatabaseConfig {
|
|
11
|
+
return {
|
|
12
|
+
database: process.env.AUTH_DATABASE_NAME ?? "__DB_NAME__",
|
|
13
|
+
host: process.env.DATABASE_HOST ?? "localhost",
|
|
14
|
+
password: process.env.DATABASE_PASSWORD ?? "postgres",
|
|
15
|
+
port: Number(process.env.DATABASE_PORT ?? 5434),
|
|
16
|
+
url: process.env.AUTH_DATABASE_URL,
|
|
17
|
+
user: process.env.DATABASE_USERNAME ?? "postgres",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAuthDatabaseConnectionString(): string {
|
|
22
|
+
const { database, host, password, port, url, user } = getAuthDatabaseConfig();
|
|
23
|
+
if (url != null && url.length > 0) {
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
`postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}` +
|
|
29
|
+
`@${host}:${port}/${encodeURIComponent(database)}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { getAuthDatabaseConnectionString } from "../config/database";
|
|
4
|
+
import * as schema from "./schema";
|
|
5
|
+
|
|
6
|
+
export const client = new Pool({
|
|
7
|
+
connectionString: getAuthDatabaseConnectionString(),
|
|
8
|
+
});
|
|
9
|
+
export const db: NodePgDatabase<typeof schema> = drizzle(client, { schema });
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
CREATE TABLE "users" (
|
|
2
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
3
|
+
"external_id" text,
|
|
4
|
+
"name" text NOT NULL,
|
|
5
|
+
"email" text NOT NULL,
|
|
6
|
+
"email_verified" boolean DEFAULT false NOT NULL,
|
|
7
|
+
"image" text,
|
|
8
|
+
"role" text DEFAULT 'user' NOT NULL,
|
|
9
|
+
"banned" boolean DEFAULT false,
|
|
10
|
+
"ban_reason" text,
|
|
11
|
+
"ban_expires" timestamp,
|
|
12
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
13
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
14
|
+
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE "account" (
|
|
18
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
19
|
+
"user_id" uuid NOT NULL,
|
|
20
|
+
"account_id" text NOT NULL,
|
|
21
|
+
"provider_id" text NOT NULL,
|
|
22
|
+
"access_token" text,
|
|
23
|
+
"refresh_token" text,
|
|
24
|
+
"expires_at" integer,
|
|
25
|
+
"access_token_expires_at" timestamp,
|
|
26
|
+
"refresh_token_expires_at" timestamp,
|
|
27
|
+
"scope" text,
|
|
28
|
+
"id_token" text,
|
|
29
|
+
"password" text,
|
|
30
|
+
"created_at" timestamp NOT NULL,
|
|
31
|
+
"updated_at" timestamp NOT NULL
|
|
32
|
+
);
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE TABLE "session" (
|
|
35
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
36
|
+
"user_id" uuid NOT NULL,
|
|
37
|
+
"token" text NOT NULL,
|
|
38
|
+
"expires_at" timestamp NOT NULL,
|
|
39
|
+
"ip_address" text,
|
|
40
|
+
"user_agent" text,
|
|
41
|
+
"impersonated_by" text,
|
|
42
|
+
"created_at" timestamp NOT NULL,
|
|
43
|
+
"updated_at" timestamp NOT NULL,
|
|
44
|
+
CONSTRAINT "session_token_unique" UNIQUE("token")
|
|
45
|
+
);
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
CREATE TABLE "verification" (
|
|
48
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
49
|
+
"identifier" text NOT NULL,
|
|
50
|
+
"value" text NOT NULL,
|
|
51
|
+
"expires_at" timestamp NOT NULL,
|
|
52
|
+
"created_at" timestamp,
|
|
53
|
+
"updated_at" timestamp
|
|
54
|
+
);
|
|
55
|
+
--> statement-breakpoint
|
|
56
|
+
CREATE TABLE "groups" (
|
|
57
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
58
|
+
"external_id" text NOT NULL,
|
|
59
|
+
"name" text NOT NULL,
|
|
60
|
+
"source" text NOT NULL,
|
|
61
|
+
"deleted_at" timestamp,
|
|
62
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
63
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
64
|
+
);
|
|
65
|
+
--> statement-breakpoint
|
|
66
|
+
CREATE TABLE "group_members" (
|
|
67
|
+
"group_id" uuid NOT NULL,
|
|
68
|
+
"user_id" uuid NOT NULL,
|
|
69
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
70
|
+
CONSTRAINT "group_members_pkey" PRIMARY KEY("group_id","user_id")
|
|
71
|
+
);
|
|
72
|
+
--> statement-breakpoint
|
|
73
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
|
74
|
+
--> statement-breakpoint
|
|
75
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
|
76
|
+
--> statement-breakpoint
|
|
77
|
+
ALTER TABLE "group_members" ADD CONSTRAINT "group_members_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "groups"("id") ON DELETE cascade ON UPDATE no action;
|
|
78
|
+
--> statement-breakpoint
|
|
79
|
+
ALTER TABLE "group_members" ADD CONSTRAINT "group_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
|
80
|
+
--> statement-breakpoint
|
|
81
|
+
CREATE UNIQUE INDEX "users_lower_email_index" ON "users" USING btree (lower("email"));
|
|
82
|
+
--> statement-breakpoint
|
|
83
|
+
CREATE UNIQUE INDEX "users_external_id_index" ON "users" USING btree ("external_id") WHERE "users"."external_id" IS NOT NULL;
|
|
84
|
+
--> statement-breakpoint
|
|
85
|
+
CREATE UNIQUE INDEX "groups_live_external_id_index" ON "groups" USING btree ("external_id") WHERE "groups"."deleted_at" IS NULL;
|
|
86
|
+
--> statement-breakpoint
|
|
87
|
+
CREATE INDEX "groups_source_index" ON "groups" USING btree ("source");
|
|
88
|
+
--> statement-breakpoint
|
|
89
|
+
CREATE INDEX "group_members_user_id_index" ON "group_members" USING btree ("user_id");
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
-
import { users } from "
|
|
2
|
+
import { users } from "../users";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Better Auth account table.
|
|
6
|
-
* Stores OAuth provider links and credential password hashes.
|
|
7
|
-
* @see https://better-auth.com/docs/concepts/database
|
|
8
|
-
*/
|
|
9
4
|
export const accounts = pgTable("account", {
|
|
10
5
|
id: text("id")
|
|
11
6
|
.$defaultFn(() => crypto.randomUUID())
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
-
import { users } from "
|
|
2
|
+
import { users } from "../users";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Better Auth session table.
|
|
6
|
-
* @see https://better-auth.com/docs/concepts/database
|
|
7
|
-
*/
|
|
8
4
|
export const sessions = pgTable("session", {
|
|
9
5
|
id: text("id")
|
|
10
6
|
.$defaultFn(() => crypto.randomUUID())
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Better Auth verification table.
|
|
5
|
-
* @see https://better-auth.com/docs/concepts/database
|
|
6
|
-
*/
|
|
7
3
|
export const verifications = pgTable("verification", {
|
|
8
4
|
id: text("id")
|
|
9
5
|
.$defaultFn(() => crypto.randomUUID())
|