chadstart 1.0.0 → 1.0.2
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/.devcontainer/devcontainer.json +34 -0
- package/.env.example +30 -1
- package/.github/workflows/db-integration.yml +139 -0
- package/.github/workflows/npm-chadstart.yml +12 -1
- package/.github/workflows/npm-sdk.yml +12 -1
- package/chadstart.example.yml +52 -0
- package/chadstart.schema.json +62 -0
- package/core/api-generator.js +36 -36
- package/core/auth.js +76 -65
- package/core/db.js +324 -149
- package/core/entity-engine.js +1 -0
- package/core/oauth.js +263 -0
- package/core/seeder.js +3 -3
- package/docs/auth.md +3 -0
- package/docs/config.md +8 -8
- package/docs/oauth.md +869 -0
- package/mkdocs.yml +1 -0
- package/package.json +5 -1
- package/server/express-server.js +20 -18
- package/test/access-policies.test.js +8 -8
- package/test/api-keys.test.js +28 -28
- package/test/auth.test.js +18 -18
- package/test/db.test.js +71 -71
- package/test/groups.test.js +5 -5
- package/test/integration/db-integration.test.js +368 -0
- package/test/middleware.test.js +1 -1
- package/test/oauth.test.js +259 -0
- package/test/sdk.test.js +19 -19
- package/test/seeder.test.js +26 -26
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chadstart",
|
|
3
|
+
"image": "mcr.microsoft.com/devcontainers/javascript-node:24-trixie",
|
|
4
|
+
"features": {},
|
|
5
|
+
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y python3 make g++ curl wget && npm ci",
|
|
6
|
+
"postCreateCommand": "cp -n .env.example .env || true",
|
|
7
|
+
"forwardPorts": [3000],
|
|
8
|
+
"portsAttributes": {
|
|
9
|
+
"3000": {
|
|
10
|
+
"label": "chadstart API",
|
|
11
|
+
"onAutoForward": "notify"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"customizations": {
|
|
15
|
+
"vscode": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"dbaeumer.vscode-eslint",
|
|
18
|
+
"esbenp.prettier-vscode",
|
|
19
|
+
"redhat.vscode-yaml",
|
|
20
|
+
"humao.rest-client",
|
|
21
|
+
"rangav.vscode-thunder-client",
|
|
22
|
+
"pkief.material-icon-theme"
|
|
23
|
+
],
|
|
24
|
+
"settings": {
|
|
25
|
+
"editor.formatOnSave": true,
|
|
26
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
27
|
+
"files.eol": "\n"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"remoteEnv": {
|
|
32
|
+
"NODE_ENV": "development"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/.env.example
CHANGED
|
@@ -14,7 +14,16 @@ TOKEN_SECRET_KEY=replace-with-a-long-random-secret
|
|
|
14
14
|
# CHADSTART_FUNCTIONS_FOLDER=functions
|
|
15
15
|
|
|
16
16
|
# Database (SQLite default)
|
|
17
|
-
#
|
|
17
|
+
# DB_ENGINE=sqlite # sqlite (default), postgres, or mysql
|
|
18
|
+
# DB_PATH=/data/chadstart.db # SQLite: path to database file
|
|
19
|
+
|
|
20
|
+
# PostgreSQL / MySQL / MariaDB (set DB_ENGINE=postgres or mysql to activate)
|
|
21
|
+
# DB_HOST=localhost
|
|
22
|
+
# DB_PORT=5432 # 5432 for PostgreSQL, 3306 for MySQL/MariaDB
|
|
23
|
+
# DB_USERNAME=postgres
|
|
24
|
+
# DB_PASSWORD=postgres
|
|
25
|
+
# DB_DATABASE=manifest
|
|
26
|
+
# DB_SSL=false # Set to true for remote managed databases
|
|
18
27
|
|
|
19
28
|
# Optional: S3-compatible storage (when set, files are stored in S3 instead of the local filesystem)
|
|
20
29
|
# S3_BUCKET=my-bucket-name
|
|
@@ -44,3 +53,23 @@ TOKEN_SECRET_KEY=replace-with-a-long-random-secret
|
|
|
44
53
|
# 💡 Bugsink (https://www.bugsink.com) is a self-hosted alternative to Sentry
|
|
45
54
|
# that uses the same Sentry SDK — just point SENTRY_DSN at your Bugsink instance.
|
|
46
55
|
# SENTRY_DSN=https://xxxxx@oXXXXX.ingest.sentry.io/XXXXXXX
|
|
56
|
+
|
|
57
|
+
# Optional: OAuth / Social Login (powered by grant)
|
|
58
|
+
# For each provider, set OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET.
|
|
59
|
+
# Provider names must be uppercase (e.g. GOOGLE, GITHUB, FACEBOOK).
|
|
60
|
+
# See docs/oauth.md for the full list of 200+ supported providers.
|
|
61
|
+
#
|
|
62
|
+
# OAUTH_GOOGLE_KEY=your-google-client-id
|
|
63
|
+
# OAUTH_GOOGLE_SECRET=your-google-client-secret
|
|
64
|
+
# OAUTH_GITHUB_KEY=your-github-client-id
|
|
65
|
+
# OAUTH_GITHUB_SECRET=your-github-client-secret
|
|
66
|
+
# OAUTH_FACEBOOK_KEY=your-facebook-app-id
|
|
67
|
+
# OAUTH_FACEBOOK_SECRET=your-facebook-app-secret
|
|
68
|
+
# OAUTH_DISCORD_KEY=your-discord-client-id
|
|
69
|
+
# OAUTH_DISCORD_SECRET=your-discord-client-secret
|
|
70
|
+
# OAUTH_APPLE_KEY=your-apple-client-id
|
|
71
|
+
# OAUTH_APPLE_SECRET=your-apple-client-secret
|
|
72
|
+
# OAUTH_MICROSOFT_KEY=your-microsoft-client-id
|
|
73
|
+
# OAUTH_MICROSOFT_SECRET=your-microsoft-client-secret
|
|
74
|
+
# OAUTH_TWITTER_KEY=your-twitter-api-key
|
|
75
|
+
# OAUTH_TWITTER_SECRET=your-twitter-api-secret
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: DB Integration Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# ── PostgreSQL ──────────────────────────────────────────────────────────────
|
|
10
|
+
test-postgres:
|
|
11
|
+
name: Integration – PostgreSQL
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
services:
|
|
17
|
+
postgres:
|
|
18
|
+
image: postgres:16
|
|
19
|
+
env:
|
|
20
|
+
POSTGRES_PASSWORD: postgres
|
|
21
|
+
POSTGRES_DB: chadstart_test
|
|
22
|
+
ports:
|
|
23
|
+
- 5432:5432
|
|
24
|
+
options: >-
|
|
25
|
+
--health-cmd pg_isready
|
|
26
|
+
--health-interval 10s
|
|
27
|
+
--health-timeout 5s
|
|
28
|
+
--health-retries 5
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- name: Checkout
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Set up Node.js
|
|
35
|
+
uses: actions/setup-node@v4
|
|
36
|
+
with:
|
|
37
|
+
node-version: "lts/*"
|
|
38
|
+
cache: "npm"
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: npm ci
|
|
42
|
+
|
|
43
|
+
- name: Run integration tests
|
|
44
|
+
run: npm run test:integration
|
|
45
|
+
env:
|
|
46
|
+
DB_ENGINE: postgres
|
|
47
|
+
DB_HOST: localhost
|
|
48
|
+
DB_PORT: 5432
|
|
49
|
+
DB_USERNAME: postgres
|
|
50
|
+
DB_PASSWORD: postgres
|
|
51
|
+
DB_DATABASE: chadstart_test
|
|
52
|
+
|
|
53
|
+
# ── MySQL ───────────────────────────────────────────────────────────────────
|
|
54
|
+
test-mysql:
|
|
55
|
+
name: Integration – MySQL
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
permissions:
|
|
58
|
+
contents: read
|
|
59
|
+
|
|
60
|
+
services:
|
|
61
|
+
mysql:
|
|
62
|
+
image: mysql:8
|
|
63
|
+
env:
|
|
64
|
+
MYSQL_ROOT_PASSWORD: root
|
|
65
|
+
MYSQL_DATABASE: chadstart_test
|
|
66
|
+
ports:
|
|
67
|
+
- 3306:3306
|
|
68
|
+
options: >-
|
|
69
|
+
--health-cmd "mysqladmin ping -h localhost -u root -proot"
|
|
70
|
+
--health-interval 10s
|
|
71
|
+
--health-timeout 5s
|
|
72
|
+
--health-retries 10
|
|
73
|
+
|
|
74
|
+
steps:
|
|
75
|
+
- name: Checkout
|
|
76
|
+
uses: actions/checkout@v4
|
|
77
|
+
|
|
78
|
+
- name: Set up Node.js
|
|
79
|
+
uses: actions/setup-node@v4
|
|
80
|
+
with:
|
|
81
|
+
node-version: "lts/*"
|
|
82
|
+
cache: "npm"
|
|
83
|
+
|
|
84
|
+
- name: Install dependencies
|
|
85
|
+
run: npm ci
|
|
86
|
+
|
|
87
|
+
- name: Run integration tests
|
|
88
|
+
run: npm run test:integration
|
|
89
|
+
env:
|
|
90
|
+
DB_ENGINE: mysql
|
|
91
|
+
DB_HOST: 127.0.0.1
|
|
92
|
+
DB_PORT: 3306
|
|
93
|
+
DB_USERNAME: root
|
|
94
|
+
DB_PASSWORD: root
|
|
95
|
+
DB_DATABASE: chadstart_test
|
|
96
|
+
|
|
97
|
+
# ── MariaDB ─────────────────────────────────────────────────────────────────
|
|
98
|
+
test-mariadb:
|
|
99
|
+
name: Integration – MariaDB
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
permissions:
|
|
102
|
+
contents: read
|
|
103
|
+
|
|
104
|
+
services:
|
|
105
|
+
mariadb:
|
|
106
|
+
image: mariadb:11
|
|
107
|
+
env:
|
|
108
|
+
MARIADB_ROOT_PASSWORD: root
|
|
109
|
+
MARIADB_DATABASE: chadstart_test
|
|
110
|
+
ports:
|
|
111
|
+
- 3306:3306
|
|
112
|
+
options: >-
|
|
113
|
+
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
|
114
|
+
--health-interval 10s
|
|
115
|
+
--health-timeout 5s
|
|
116
|
+
--health-retries 10
|
|
117
|
+
|
|
118
|
+
steps:
|
|
119
|
+
- name: Checkout
|
|
120
|
+
uses: actions/checkout@v4
|
|
121
|
+
|
|
122
|
+
- name: Set up Node.js
|
|
123
|
+
uses: actions/setup-node@v4
|
|
124
|
+
with:
|
|
125
|
+
node-version: "lts/*"
|
|
126
|
+
cache: "npm"
|
|
127
|
+
|
|
128
|
+
- name: Install dependencies
|
|
129
|
+
run: npm ci
|
|
130
|
+
|
|
131
|
+
- name: Run integration tests
|
|
132
|
+
run: npm run test:integration
|
|
133
|
+
env:
|
|
134
|
+
DB_ENGINE: mysql
|
|
135
|
+
DB_HOST: 127.0.0.1
|
|
136
|
+
DB_PORT: 3306
|
|
137
|
+
DB_USERNAME: root
|
|
138
|
+
DB_PASSWORD: root
|
|
139
|
+
DB_DATABASE: chadstart_test
|
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
name: NPM Publish
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
permissions:
|
|
12
|
-
contents:
|
|
12
|
+
contents: write
|
|
13
13
|
id-token: write # required for npm provenance
|
|
14
14
|
steps:
|
|
15
15
|
- name: Checkout repository
|
|
@@ -21,6 +21,17 @@ jobs:
|
|
|
21
21
|
node-version: '24'
|
|
22
22
|
registry-url: 'https://registry.npmjs.org'
|
|
23
23
|
|
|
24
|
+
- name: Configure git
|
|
25
|
+
run: |
|
|
26
|
+
git config user.name "github-actions[bot]"
|
|
27
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
28
|
+
|
|
29
|
+
- name: Bump patch version
|
|
30
|
+
run: npm version patch --message "v%s [skip ci]"
|
|
31
|
+
|
|
32
|
+
- name: Push version bump
|
|
33
|
+
run: git push --follow-tags
|
|
34
|
+
|
|
24
35
|
- name: Publish to npm
|
|
25
36
|
run: npm publish --access public --provenance
|
|
26
37
|
env:
|
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
run:
|
|
15
15
|
working-directory: sdk
|
|
16
16
|
permissions:
|
|
17
|
-
contents:
|
|
17
|
+
contents: write
|
|
18
18
|
id-token: write # required for npm provenance
|
|
19
19
|
steps:
|
|
20
20
|
- name: Checkout repository
|
|
@@ -32,6 +32,17 @@ jobs:
|
|
|
32
32
|
- name: Run SDK tests
|
|
33
33
|
run: node test/sdk.test.cjs
|
|
34
34
|
|
|
35
|
+
- name: Configure git
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
|
|
40
|
+
- name: Bump patch version
|
|
41
|
+
run: npm version patch --message "v%s [skip ci]"
|
|
42
|
+
|
|
43
|
+
- name: Push version bump
|
|
44
|
+
run: git push --follow-tags
|
|
45
|
+
|
|
35
46
|
- name: Publish to npm
|
|
36
47
|
run: npm publish --access public --provenance
|
|
37
48
|
env:
|
package/chadstart.example.yml
CHANGED
|
@@ -414,3 +414,55 @@ sentry:
|
|
|
414
414
|
environment: production # Label sent to Sentry. Defaults to NODE_ENV.
|
|
415
415
|
tracesSampleRate: 1.0 # Fraction of transactions to sample (0.0–1.0)
|
|
416
416
|
debug: false # Enable Sentry SDK debug logging
|
|
417
|
+
|
|
418
|
+
# ── OAuth / Social Login ─────────────────────────────────────────────────────
|
|
419
|
+
# Powered by the "grant" library — supports 200+ OAuth providers.
|
|
420
|
+
# Secrets (client keys / secrets) MUST be set via environment variables:
|
|
421
|
+
# OAUTH_<PROVIDER>_KEY — client / app ID
|
|
422
|
+
# OAUTH_<PROVIDER>_SECRET — client / app secret
|
|
423
|
+
# See docs/oauth.md for the full provider list and setup guides.
|
|
424
|
+
|
|
425
|
+
oauth:
|
|
426
|
+
# Which authenticable entity to create/find users in (default: first authenticable entity).
|
|
427
|
+
entity: User
|
|
428
|
+
|
|
429
|
+
# Where to redirect after successful login. The JWT token is appended as ?token=...
|
|
430
|
+
# If omitted, the callback returns JSON instead.
|
|
431
|
+
successRedirect: /login?success=true
|
|
432
|
+
|
|
433
|
+
# Where to redirect on error. The error message is appended as ?error=...
|
|
434
|
+
errorRedirect: /login?error=true
|
|
435
|
+
|
|
436
|
+
# Default settings applied to all providers.
|
|
437
|
+
defaults:
|
|
438
|
+
transport: querystring
|
|
439
|
+
|
|
440
|
+
# Configure each provider you want to support.
|
|
441
|
+
# The provider names must match grant's provider names (lowercase).
|
|
442
|
+
# Full list: https://github.com/simov/grant#200-supported-providers
|
|
443
|
+
providers:
|
|
444
|
+
google:
|
|
445
|
+
scope:
|
|
446
|
+
- openid
|
|
447
|
+
- email
|
|
448
|
+
- profile
|
|
449
|
+
custom_params:
|
|
450
|
+
access_type: offline
|
|
451
|
+
# key and secret via: OAUTH_GOOGLE_KEY, OAUTH_GOOGLE_SECRET
|
|
452
|
+
|
|
453
|
+
github:
|
|
454
|
+
scope:
|
|
455
|
+
- user:email
|
|
456
|
+
# key and secret via: OAUTH_GITHUB_KEY, OAUTH_GITHUB_SECRET
|
|
457
|
+
|
|
458
|
+
# facebook:
|
|
459
|
+
# scope:
|
|
460
|
+
# - email
|
|
461
|
+
# - public_profile
|
|
462
|
+
# key and secret via: OAUTH_FACEBOOK_KEY, OAUTH_FACEBOOK_SECRET
|
|
463
|
+
|
|
464
|
+
# discord:
|
|
465
|
+
# scope:
|
|
466
|
+
# - identify
|
|
467
|
+
# - email
|
|
468
|
+
# key and secret via: OAUTH_DISCORD_KEY, OAUTH_DISCORD_SECRET
|
package/chadstart.schema.json
CHANGED
|
@@ -124,6 +124,43 @@
|
|
|
124
124
|
"description": "Enable Sentry SDK debug logging."
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
},
|
|
128
|
+
"oauth": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"description": "OAuth / social login configuration powered by the grant library. Secrets (client keys and secrets) must be supplied via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"properties": {
|
|
133
|
+
"entity": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "Name of the authenticable entity to use for OAuth users (e.g. 'User'). Defaults to the first authenticable entity."
|
|
136
|
+
},
|
|
137
|
+
"successRedirect": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "URL to redirect to after successful OAuth login. The JWT token is appended as a ?token= query parameter. If omitted, returns JSON."
|
|
140
|
+
},
|
|
141
|
+
"errorRedirect": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "URL to redirect to on OAuth error. The error message is appended as an ?error= query parameter. If omitted, returns JSON error."
|
|
144
|
+
},
|
|
145
|
+
"defaults": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"description": "Default settings applied to all providers (e.g. transport, scope).",
|
|
148
|
+
"properties": {
|
|
149
|
+
"transport": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"enum": ["querystring", "session"],
|
|
152
|
+
"default": "querystring"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"providers": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"description": "Map of OAuth provider names to their configuration. Provider names must match grant's supported provider list (e.g. google, github, facebook). See https://www.npmjs.com/package/grant for all 200+ supported providers.",
|
|
159
|
+
"additionalProperties": {
|
|
160
|
+
"$ref": "#/$defs/oauthProvider"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
127
164
|
}
|
|
128
165
|
},
|
|
129
166
|
"$defs": {
|
|
@@ -362,6 +399,31 @@
|
|
|
362
399
|
"limit": { "type": "integer", "description": "Maximum number of requests allowed in the time window." },
|
|
363
400
|
"ttl": { "type": "integer", "description": "Time window in milliseconds." }
|
|
364
401
|
}
|
|
402
|
+
},
|
|
403
|
+
"oauthProvider": {
|
|
404
|
+
"type": "object",
|
|
405
|
+
"description": "Configuration for a single OAuth provider. The key and secret should be set via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
|
|
406
|
+
"properties": {
|
|
407
|
+
"key": { "type": "string", "description": "OAuth client/app ID. Prefer using OAUTH_<PROVIDER>_KEY env var instead." },
|
|
408
|
+
"secret": { "type": "string", "description": "OAuth client/app secret. Prefer using OAUTH_<PROVIDER>_SECRET env var instead." },
|
|
409
|
+
"scope": {
|
|
410
|
+
"oneOf": [
|
|
411
|
+
{ "type": "string" },
|
|
412
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
413
|
+
],
|
|
414
|
+
"description": "OAuth scopes to request (e.g. 'openid email profile')."
|
|
415
|
+
},
|
|
416
|
+
"callback": { "type": "string", "description": "Custom callback URL path. Defaults to /api/auth/oauth/callback." },
|
|
417
|
+
"custom_params": { "type": "object", "description": "Extra query parameters to send to the authorization URL." },
|
|
418
|
+
"subdomain": { "type": "string", "description": "Subdomain for providers that require one (e.g. Shopify)." },
|
|
419
|
+
"nonce": { "type": "boolean", "description": "Enable nonce generation (required by some OIDC providers)." },
|
|
420
|
+
"pkce": { "type": "boolean", "description": "Enable PKCE (Proof Key for Code Exchange) for enhanced security." },
|
|
421
|
+
"response": {
|
|
422
|
+
"type": "array",
|
|
423
|
+
"items": { "type": "string" },
|
|
424
|
+
"description": "Data to include in the callback (e.g. ['tokens', 'profile'])."
|
|
425
|
+
}
|
|
426
|
+
}
|
|
365
427
|
}
|
|
366
428
|
}
|
|
367
429
|
}
|
package/core/api-generator.js
CHANGED
|
@@ -38,17 +38,17 @@ function createBackendSdk(core) {
|
|
|
38
38
|
if (!entity) throw new Error(`Single entity not found for slug: ${slug}`);
|
|
39
39
|
const table = entity.tableName;
|
|
40
40
|
return {
|
|
41
|
-
get() {
|
|
42
|
-
const rows = db.findAllSimple(table);
|
|
41
|
+
async get() {
|
|
42
|
+
const rows = await db.findAllSimple(table);
|
|
43
43
|
return rows[0] || null;
|
|
44
44
|
},
|
|
45
|
-
update(data) {
|
|
46
|
-
const rows = db.findAllSimple(table);
|
|
45
|
+
async update(data) {
|
|
46
|
+
const rows = await db.findAllSimple(table);
|
|
47
47
|
if (!rows[0]) return null;
|
|
48
48
|
return db.update(table, rows[0].id, data);
|
|
49
49
|
},
|
|
50
|
-
patch(data) {
|
|
51
|
-
const rows = db.findAllSimple(table);
|
|
50
|
+
async patch(data) {
|
|
51
|
+
const rows = await db.findAllSimple(table);
|
|
52
52
|
if (!rows[0]) return null;
|
|
53
53
|
return db.update(table, rows[0].id, data);
|
|
54
54
|
},
|
|
@@ -82,9 +82,9 @@ function registerApiRoutes(app, core, emit) {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
// GET single
|
|
85
|
-
router.get(base, mw.read, (_req, res) => {
|
|
85
|
+
router.get(base, mw.read, async (_req, res) => {
|
|
86
86
|
try {
|
|
87
|
-
const rows = db.findAllSimple(table);
|
|
87
|
+
const rows = await db.findAllSimple(table);
|
|
88
88
|
const row = rows[0];
|
|
89
89
|
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
90
90
|
res.json(hide(row));
|
|
@@ -94,7 +94,7 @@ function registerApiRoutes(app, core, emit) {
|
|
|
94
94
|
// PUT single (full replace)
|
|
95
95
|
router.put(base, mw.update, async (req, res) => {
|
|
96
96
|
try {
|
|
97
|
-
const rows = db.findAllSimple(table);
|
|
97
|
+
const rows = await db.findAllSimple(table);
|
|
98
98
|
const row = rows[0];
|
|
99
99
|
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
100
100
|
if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
|
|
@@ -102,7 +102,7 @@ function registerApiRoutes(app, core, emit) {
|
|
|
102
102
|
if (v.errors) return res.status(400).json(v.errors);
|
|
103
103
|
fireWebhooks(entity, 'beforeUpdate', req.body);
|
|
104
104
|
const sanitized = sanitizeBody(req.body, entity, true);
|
|
105
|
-
const updated = db.update(table, row.id, sanitized);
|
|
105
|
+
const updated = await db.update(table, row.id, sanitized);
|
|
106
106
|
fireWebhooks(entity, 'afterUpdate', updated);
|
|
107
107
|
await runMiddlewares('afterUpdate', entity, req, res, sdk);
|
|
108
108
|
emit(`${entity.name}.updated`, hide(updated));
|
|
@@ -113,14 +113,14 @@ function registerApiRoutes(app, core, emit) {
|
|
|
113
113
|
// PATCH single (partial)
|
|
114
114
|
router.patch(base, mw.update, async (req, res) => {
|
|
115
115
|
try {
|
|
116
|
-
const rows = db.findAllSimple(table);
|
|
116
|
+
const rows = await db.findAllSimple(table);
|
|
117
117
|
const row = rows[0];
|
|
118
118
|
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
119
119
|
if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
|
|
120
120
|
const v = validateBody(req.body, entity, core.groups, { partial: true });
|
|
121
121
|
if (v.errors) return res.status(400).json(v.errors);
|
|
122
122
|
fireWebhooks(entity, 'beforeUpdate', req.body);
|
|
123
|
-
const updated = db.update(table, row.id, sanitizeBody(req.body, entity));
|
|
123
|
+
const updated = await db.update(table, row.id, sanitizeBody(req.body, entity));
|
|
124
124
|
fireWebhooks(entity, 'afterUpdate', updated);
|
|
125
125
|
await runMiddlewares('afterUpdate', entity, req, res, sdk);
|
|
126
126
|
emit(`${entity.name}.updated`, hide(updated));
|
|
@@ -140,37 +140,37 @@ function registerApiRoutes(app, core, emit) {
|
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
// GET list (paginated)
|
|
143
|
-
router.get(base, mw.read, (req, res) => {
|
|
143
|
+
router.get(base, mw.read, async (req, res) => {
|
|
144
144
|
try {
|
|
145
145
|
// Ownership filter: condition: self forces a FK filter on the current user
|
|
146
146
|
const query = req._selfFilter
|
|
147
147
|
? { ...req.query, [req._selfFilter.fk]: req._selfFilter.userId }
|
|
148
148
|
: req.query;
|
|
149
|
-
const result = db.findAll(table, query, {
|
|
149
|
+
const result = await db.findAll(table, query, {
|
|
150
150
|
page: req.query.page,
|
|
151
151
|
perPage: req.query.perPage,
|
|
152
152
|
orderBy: req.query.orderBy,
|
|
153
153
|
order: req.query.order,
|
|
154
154
|
});
|
|
155
155
|
const relations = req.query.relations;
|
|
156
|
-
|
|
157
|
-
if (relations) db.loadRelations(row, entity, relations);
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
for (const row of result.data) {
|
|
157
|
+
if (relations) await db.loadRelations(row, entity, relations);
|
|
158
|
+
}
|
|
159
|
+
result.data = result.data.map((row) => hide(row));
|
|
160
160
|
res.json(result);
|
|
161
161
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
// GET single by id
|
|
165
|
-
router.get(`${base}/:id`, mw.read, (req, res) => {
|
|
165
|
+
router.get(`${base}/:id`, mw.read, async (req, res) => {
|
|
166
166
|
try {
|
|
167
|
-
const row = db.findById(table, req.params.id);
|
|
167
|
+
const row = await db.findById(table, req.params.id);
|
|
168
168
|
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
169
169
|
// Ownership check for read with condition: self
|
|
170
170
|
if (req._selfFilter && row[req._selfFilter.fk] !== req._selfFilter.userId) {
|
|
171
171
|
return res.status(403).json({ error: 'Access denied' });
|
|
172
172
|
}
|
|
173
|
-
if (req.query.relations) db.loadRelations(row, entity, req.query.relations);
|
|
173
|
+
if (req.query.relations) await db.loadRelations(row, entity, req.query.relations);
|
|
174
174
|
res.json(hide(row));
|
|
175
175
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
176
176
|
});
|
|
@@ -183,8 +183,8 @@ function registerApiRoutes(app, core, emit) {
|
|
|
183
183
|
const v = validateBody(body, entity, core.groups);
|
|
184
184
|
if (v.errors) return res.status(400).json(v.errors);
|
|
185
185
|
fireWebhooks(entity, 'beforeCreate', body);
|
|
186
|
-
const row = db.create(table, sanitizeBody(body, entity));
|
|
187
|
-
db.saveBelongsToMany(entity, row.id, req.body);
|
|
186
|
+
const row = await db.create(table, sanitizeBody(body, entity));
|
|
187
|
+
await db.saveBelongsToMany(entity, row.id, req.body);
|
|
188
188
|
fireWebhooks(entity, 'afterCreate', row);
|
|
189
189
|
await runMiddlewares('afterCreate', entity, req, res, sdk);
|
|
190
190
|
emit(`${entity.name}.created`, hide(row));
|
|
@@ -195,14 +195,14 @@ function registerApiRoutes(app, core, emit) {
|
|
|
195
195
|
// PUT full replace
|
|
196
196
|
router.put(`${base}/:id`, mw.update, async (req, res) => {
|
|
197
197
|
try {
|
|
198
|
-
if (!db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
|
|
198
|
+
if (!await db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
|
|
199
199
|
if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
|
|
200
200
|
const v = validateBody(req.body, entity, core.groups);
|
|
201
201
|
if (v.errors) return res.status(400).json(v.errors);
|
|
202
202
|
fireWebhooks(entity, 'beforeUpdate', req.body);
|
|
203
203
|
const sanitized = sanitizeBody(req.body, entity, true);
|
|
204
|
-
const row = db.update(table, req.params.id, sanitized);
|
|
205
|
-
db.saveBelongsToMany(entity, row.id, req.body);
|
|
204
|
+
const row = await db.update(table, req.params.id, sanitized);
|
|
205
|
+
await db.saveBelongsToMany(entity, row.id, req.body);
|
|
206
206
|
fireWebhooks(entity, 'afterUpdate', row);
|
|
207
207
|
await runMiddlewares('afterUpdate', entity, req, res, sdk);
|
|
208
208
|
emit(`${entity.name}.updated`, hide(row));
|
|
@@ -213,13 +213,13 @@ function registerApiRoutes(app, core, emit) {
|
|
|
213
213
|
// PATCH partial update
|
|
214
214
|
router.patch(`${base}/:id`, mw.update, async (req, res) => {
|
|
215
215
|
try {
|
|
216
|
-
if (!db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
|
|
216
|
+
if (!await db.findById(table, req.params.id)) return res.status(404).json({ error: 'Not found' });
|
|
217
217
|
if (!await runMiddlewares('beforeUpdate', entity, req, res, sdk)) return;
|
|
218
218
|
const v = validateBody(req.body, entity, core.groups, { partial: true });
|
|
219
219
|
if (v.errors) return res.status(400).json(v.errors);
|
|
220
220
|
fireWebhooks(entity, 'beforeUpdate', req.body);
|
|
221
|
-
const row = db.update(table, req.params.id, sanitizeBody(req.body, entity));
|
|
222
|
-
db.saveBelongsToMany(entity, row.id, req.body);
|
|
221
|
+
const row = await db.update(table, req.params.id, sanitizeBody(req.body, entity));
|
|
222
|
+
await db.saveBelongsToMany(entity, row.id, req.body);
|
|
223
223
|
fireWebhooks(entity, 'afterUpdate', row);
|
|
224
224
|
await runMiddlewares('afterUpdate', entity, req, res, sdk);
|
|
225
225
|
emit(`${entity.name}.updated`, hide(row));
|
|
@@ -230,11 +230,11 @@ function registerApiRoutes(app, core, emit) {
|
|
|
230
230
|
// DELETE
|
|
231
231
|
router.delete(`${base}/:id`, mw.delete, async (req, res) => {
|
|
232
232
|
try {
|
|
233
|
-
const existing = db.findById(table, req.params.id);
|
|
233
|
+
const existing = await db.findById(table, req.params.id);
|
|
234
234
|
if (!existing) return res.status(404).json({ error: 'Not found' });
|
|
235
235
|
if (!await runMiddlewares('beforeDelete', entity, req, res, sdk)) return;
|
|
236
236
|
fireWebhooks(entity, 'beforeDelete', existing);
|
|
237
|
-
const row = db.remove(table, req.params.id);
|
|
237
|
+
const row = await db.remove(table, req.params.id);
|
|
238
238
|
fireWebhooks(entity, 'afterDelete', row);
|
|
239
239
|
await runMiddlewares('afterDelete', entity, req, res, sdk);
|
|
240
240
|
emit(`${entity.name}.deleted`, hide(row));
|
|
@@ -267,8 +267,8 @@ function policyMiddleware(rule, entity, core) {
|
|
|
267
267
|
break;
|
|
268
268
|
}
|
|
269
269
|
const allowed = Array.isArray(p.allow) ? p.allow : [p.allow];
|
|
270
|
-
middlewares = [(req, res, next) => {
|
|
271
|
-
const { user, apiKeyPermissions, error } = resolveAuthHeader(req.headers.authorization);
|
|
270
|
+
middlewares = [async (req, res, next) => {
|
|
271
|
+
const { user, apiKeyPermissions, error } = await resolveAuthHeader(req.headers.authorization);
|
|
272
272
|
if (!user) return res.status(401).json({ error: 'Authorization required' });
|
|
273
273
|
if (error === 'invalid_token') return res.status(401).json({ error: 'Invalid or expired token' });
|
|
274
274
|
if (!allowed.includes(user.entity)) return res.status(403).json({ error: 'Access denied' });
|
|
@@ -277,7 +277,7 @@ function policyMiddleware(rule, entity, core) {
|
|
|
277
277
|
try {
|
|
278
278
|
// Ownership-based access: condition: self
|
|
279
279
|
if (p.condition === 'self') {
|
|
280
|
-
enforceSelfCondition(rule, entity, req, core);
|
|
280
|
+
await enforceSelfCondition(rule, entity, req, core);
|
|
281
281
|
}
|
|
282
282
|
next();
|
|
283
283
|
} catch (e) {
|
|
@@ -328,7 +328,7 @@ function _apiKeyPermGuard(operation, entity) {
|
|
|
328
328
|
* - update: ensure the record belongs to the user, disallow ownership change
|
|
329
329
|
* - delete: ensure the record belongs to the user
|
|
330
330
|
*/
|
|
331
|
-
function enforceSelfCondition(rule, entity, req, core) {
|
|
331
|
+
async function enforceSelfCondition(rule, entity, req, core) {
|
|
332
332
|
const userId = req.user.id;
|
|
333
333
|
const userEntity = req.user.entity;
|
|
334
334
|
|
|
@@ -351,7 +351,7 @@ function enforceSelfCondition(rule, entity, req, core) {
|
|
|
351
351
|
} else if (rule === 'update' || rule === 'delete') {
|
|
352
352
|
// Verify the record belongs to the user
|
|
353
353
|
if (req.params && req.params.id) {
|
|
354
|
-
const row = db.findById(entity.tableName, req.params.id);
|
|
354
|
+
const row = await db.findById(entity.tableName, req.params.id);
|
|
355
355
|
if (row && row[fk] !== userId) {
|
|
356
356
|
const err = new Error('Access denied: record does not belong to you');
|
|
357
357
|
err.status = 403;
|