@rapidd/core 2.1.0 → 2.1.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/.env.example +3 -3
- package/README.md +49 -17
- package/bin/cli.js +23 -3
- package/config/app.json +3 -0
- package/main.ts +6 -0
- package/package.json +2 -2
- package/routes/api/v1/index.ts +1 -2
- package/src/app.ts +7 -0
- package/src/auth/Auth.ts +4 -4
- package/src/index.ts +1 -1
- package/src/orm/Model.ts +6 -2
- package/src/orm/QueryBuilder.ts +26 -9
- package/src/plugins/auth.ts +43 -8
- package/src/types.ts +3 -3
package/.env.example
CHANGED
|
@@ -40,10 +40,10 @@ DB_USER_PASSWORD_FIELD=
|
|
|
40
40
|
# Auto-detected from unique string fields; comma-separated (default: email)
|
|
41
41
|
DB_USER_IDENTIFIER_FIELDS=
|
|
42
42
|
# Comma-separated: bearer, basic, cookie, header (default: bearer)
|
|
43
|
-
|
|
44
|
-
# Cookie name for cookie
|
|
43
|
+
AUTH_METHODS=bearer
|
|
44
|
+
# Cookie name for cookie auth method (default: token)
|
|
45
45
|
AUTH_COOKIE_NAME=token
|
|
46
|
-
# Header name for header
|
|
46
|
+
# Header name for header auth method (default: X-Auth-Token)
|
|
47
47
|
AUTH_CUSTOM_HEADER=X-Auth-Token
|
|
48
48
|
|
|
49
49
|
# ── API Settings ───────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Code-first REST API framework for TypeScript. Database in, API out.
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@rapidd/core)
|
|
7
8
|
[](https://www.typescriptlang.org/)
|
|
8
9
|
[](https://fastify.dev/)
|
|
9
10
|
[](https://www.prisma.io/)
|
|
@@ -29,7 +30,9 @@ Rapidd generates a fully-featured REST API from your database schema — then ge
|
|
|
29
30
|
## Quick Start
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
|
|
33
|
+
mkdir my-api && cd my-api
|
|
34
|
+
npx rapidd create-project # scaffold project files
|
|
35
|
+
npm install
|
|
33
36
|
```
|
|
34
37
|
|
|
35
38
|
```env
|
|
@@ -44,7 +47,7 @@ npm run dev # http://localhost:3000
|
|
|
44
47
|
|
|
45
48
|
Every table gets full CRUD endpoints. Auth is enabled automatically when a user table is detected. Every auto-detected value — auth fields, password hashing, JWT secrets, session store — is overridable via env vars. See [`.env.example`](.env.example) for the full list.
|
|
46
49
|
|
|
47
|
-
>
|
|
50
|
+
> **[Getting Started guide](https://github.com/MertDalbudak/rapidd/wiki/Getting-Started)** — full walkthrough with project structure
|
|
48
51
|
|
|
49
52
|
---
|
|
50
53
|
|
|
@@ -56,7 +59,7 @@ Every table gets full CRUD endpoints. Auth is enabled automatically when a user
|
|
|
56
59
|
| Query filtering (20+ operators) | ✓ | ✓ |
|
|
57
60
|
| Relations & deep includes | ✓ | ✓ |
|
|
58
61
|
| Field selection | ✓ | ✓ |
|
|
59
|
-
| JWT authentication (4
|
|
62
|
+
| JWT authentication (4 methods) | ✓ | ✓ |
|
|
60
63
|
| Per-model ACL | ✓ | ✓ |
|
|
61
64
|
| Row-Level Security (database-enforced) | ✓ | — |
|
|
62
65
|
| Rate limiting (Redis + memory fallback) | ✓ | ✓ |
|
|
@@ -83,7 +86,7 @@ GET /api/v1/posts?sortBy=createdAt&sortOrder=desc&limit=10&offset=20
|
|
|
83
86
|
|
|
84
87
|
20+ filter operators for strings, numbers, dates, arrays, nulls, and nested relation fields. Responses include pagination metadata with `total`, `count`, `limit`, `offset`, and `hasMore`.
|
|
85
88
|
|
|
86
|
-
>
|
|
89
|
+
> **[Query API wiki](https://github.com/MertDalbudak/rapidd/wiki/Query-API)** — all operators, composite PKs, relation filtering
|
|
87
90
|
|
|
88
91
|
---
|
|
89
92
|
|
|
@@ -98,11 +101,24 @@ POST /auth/refresh { "refreshToken": "..." }
|
|
|
98
101
|
GET /auth/me Authorization: Bearer <token>
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
Four
|
|
104
|
+
Four methods — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable globally via `AUTH_METHODS` env var or per endpoint prefix in `config/app.json`:
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"endpointAuthMethod": {
|
|
109
|
+
"/api/v1": ["basic", "bearer"],
|
|
110
|
+
"/api/v2": "bearer"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Set `null` for the global default, a string for a single method, or an array for multiple. Route-level config takes highest priority, then prefix match, then global default.
|
|
116
|
+
|
|
117
|
+
Multi-identifier login lets users authenticate with any unique field (email, username, phone) in a single endpoint.
|
|
104
118
|
|
|
105
|
-
|
|
119
|
+
**Production:** `JWT_SECRET` and `JWT_REFRESH_SECRET` must be set explicitly. The server refuses to start without them to prevent session invalidation on restart.
|
|
120
|
+
|
|
121
|
+
> **[Authentication wiki](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** — session stores, route protection, per-endpoint method overrides
|
|
106
122
|
|
|
107
123
|
---
|
|
108
124
|
|
|
@@ -126,7 +142,7 @@ const acl: AclConfig = {
|
|
|
126
142
|
|
|
127
143
|
Return `{}` for full access, a filter object to scope records, or `false` to deny.
|
|
128
144
|
|
|
129
|
-
>
|
|
145
|
+
> **[Access Control wiki](https://github.com/MertDalbudak/rapidd/wiki/Access-Control-(ACL))** — all 5 ACL methods, relation ACL, 404 vs 403 distinction
|
|
130
146
|
|
|
131
147
|
---
|
|
132
148
|
|
|
@@ -152,7 +168,7 @@ Model.middleware.use('before', 'delete', async (ctx) => {
|
|
|
152
168
|
|
|
153
169
|
Supports `create`, `update`, `upsert`, `upsertMany`, `delete`, `get`, `getMany`, and `count`. Middleware can abort operations, modify data, and short-circuit with cached results.
|
|
154
170
|
|
|
155
|
-
>
|
|
171
|
+
> **[Model Middleware wiki](https://github.com/MertDalbudak/rapidd/wiki/Model-Middleware)** — all hooks, context object, patterns (soft delete, validation, caching)
|
|
156
172
|
|
|
157
173
|
---
|
|
158
174
|
|
|
@@ -173,7 +189,7 @@ CREATE POLICY tenant_isolation ON orders
|
|
|
173
189
|
USING (tenant_id = current_setting('app.current_tenant_id')::int);
|
|
174
190
|
```
|
|
175
191
|
|
|
176
|
-
>
|
|
192
|
+
> **[Row-Level Security wiki](https://github.com/MertDalbudak/rapidd/wiki/Row%E2%80%90Level-Security-(RLS))** — policy examples, RLS vs ACL comparison
|
|
177
193
|
|
|
178
194
|
---
|
|
179
195
|
|
|
@@ -181,11 +197,11 @@ CREATE POLICY tenant_isolation ON orders
|
|
|
181
197
|
|
|
182
198
|
| Utility | Description | Docs |
|
|
183
199
|
|---------|-------------|------|
|
|
184
|
-
| **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki
|
|
185
|
-
| **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki
|
|
186
|
-
| **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki
|
|
187
|
-
| **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki
|
|
188
|
-
| **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki
|
|
200
|
+
| **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/ApiClient) |
|
|
201
|
+
| **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Mailer) |
|
|
202
|
+
| **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/File-Uploads) |
|
|
203
|
+
| **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Rate-Limiting) |
|
|
204
|
+
| **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Internationalization-(i18n)) |
|
|
189
205
|
|
|
190
206
|
---
|
|
191
207
|
|
|
@@ -203,12 +219,28 @@ TRUST_PROXY=true
|
|
|
203
219
|
npm run build && npm start
|
|
204
220
|
|
|
205
221
|
# or Docker
|
|
206
|
-
docker build -t
|
|
222
|
+
docker build -t my-api . && docker run -p 3000:3000 --env-file .env my-api
|
|
207
223
|
```
|
|
208
224
|
|
|
209
225
|
**Security defaults in production:** HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, and CORS with explicit origin whitelisting — all enabled automatically.
|
|
210
226
|
|
|
211
|
-
>
|
|
227
|
+
> **[Deployment wiki](https://github.com/MertDalbudak/rapidd/wiki/Deployment-&-Production)** — Docker Compose, nginx reverse proxy, production checklist, horizontal scaling
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Packages
|
|
232
|
+
|
|
233
|
+
| Package | Description |
|
|
234
|
+
|---------|-------------|
|
|
235
|
+
| [`@rapidd/core`](https://www.npmjs.com/package/@rapidd/core) | Framework runtime, project scaffolding, and unified `npx rapidd` CLI |
|
|
236
|
+
| [`@rapidd/build`](https://www.npmjs.com/package/@rapidd/build) | Code generation — models, routes, and ACL from your Prisma schema |
|
|
237
|
+
|
|
238
|
+
All commands go through `npx rapidd`:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx rapidd create-project # scaffold a new project (@rapidd/core)
|
|
242
|
+
npx rapidd build # generate from schema (@rapidd/build)
|
|
243
|
+
```
|
|
212
244
|
|
|
213
245
|
---
|
|
214
246
|
|
package/bin/cli.js
CHANGED
|
@@ -2,20 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
5
6
|
|
|
6
7
|
const COMMANDS = { 'create-project': createProject };
|
|
7
8
|
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
10
|
const command = args[0];
|
|
10
11
|
|
|
11
|
-
if (!command
|
|
12
|
+
if (!command) {
|
|
12
13
|
console.log('Usage: npx rapidd <command>\n');
|
|
13
14
|
console.log('Commands:');
|
|
14
15
|
console.log(' create-project Scaffold a new Rapidd project in the current directory');
|
|
15
|
-
|
|
16
|
+
console.log(' build Generate models, routes & ACL from Prisma schema (@rapidd/build)');
|
|
17
|
+
process.exit(0);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
COMMANDS[command]
|
|
20
|
+
if (COMMANDS[command]) {
|
|
21
|
+
COMMANDS[command](args.slice(1));
|
|
22
|
+
} else if (command === 'build') {
|
|
23
|
+
// Proxy to @rapidd/build
|
|
24
|
+
try {
|
|
25
|
+
const buildBin = require.resolve('@rapidd/build/bin/cli.js');
|
|
26
|
+
execFileSync(process.execPath, [buildBin, ...args], { stdio: 'inherit' });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
29
|
+
console.error('@rapidd/build is not installed.\n');
|
|
30
|
+
console.error(' npm install -D @rapidd/build');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
process.exit(err.status ?? 1);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
console.error(`Unknown command: ${command}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
19
39
|
|
|
20
40
|
function createProject() {
|
|
21
41
|
const targetDir = process.cwd();
|
package/config/app.json
CHANGED
package/main.ts
CHANGED
|
@@ -16,6 +16,12 @@ export async function start(): Promise<void> {
|
|
|
16
16
|
await app.listen({ port, host });
|
|
17
17
|
console.log(`[Rapidd] Server running at http://${host}:${port}`);
|
|
18
18
|
console.log(`[Rapidd] Environment: ${getEnv('NODE_ENV')}`);
|
|
19
|
+
|
|
20
|
+
// Warn if running compiled build with development NODE_ENV
|
|
21
|
+
if (process.argv[1]?.includes('/dist/') && getEnv('NODE_ENV') === 'development') {
|
|
22
|
+
console.warn('[Rapidd] Warning: Running compiled build with NODE_ENV=development.');
|
|
23
|
+
console.warn('[Rapidd] Set NODE_ENV=production in your .env for production use.');
|
|
24
|
+
}
|
|
19
25
|
} catch (err) {
|
|
20
26
|
console.error('[Startup Error]', (err as Error).message);
|
|
21
27
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "2.1.
|
|
6
|
+
"version": "2.1.2",
|
|
7
7
|
"description": "Code-first REST API framework for TypeScript. Database in, API out.",
|
|
8
8
|
"main": "dist/main.js",
|
|
9
9
|
"bin": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"pg": "^8.16.3"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@rapidd/build": "^2.1.
|
|
86
|
+
"@rapidd/build": "^2.1.5",
|
|
87
87
|
"@types/bcrypt": "^6.0.0",
|
|
88
88
|
"@types/ejs": "^3.1.5",
|
|
89
89
|
"@types/jest": "^30.0.0",
|
package/routes/api/v1/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
-
import { Auth } from '../../../src/auth/Auth';
|
|
3
2
|
import { ErrorResponse } from '../../../src/core/errors';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Auth routes — /register, /login, /logout, /refresh, /me
|
|
7
6
|
*/
|
|
8
7
|
const rootRoutes: FastifyPluginAsync = async (fastify) => {
|
|
9
|
-
const auth = fastify.auth
|
|
8
|
+
const auth = fastify.auth;
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* POST /register
|
package/src/app.ts
CHANGED
|
@@ -22,6 +22,13 @@ import rlsPlugin from './plugins/rls';
|
|
|
22
22
|
|
|
23
23
|
import type { RapiddOptions } from './types';
|
|
24
24
|
|
|
25
|
+
// ─── BigInt Serialization ────────────────────────────
|
|
26
|
+
// Prisma returns BigInt values that JSON.stringify cannot handle natively.
|
|
27
|
+
// This polyfill converts them to strings during serialization.
|
|
28
|
+
(BigInt.prototype as any).toJSON = function () {
|
|
29
|
+
return this.toString();
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
// ─── Path Setup ─────────────────────────────────────
|
|
26
33
|
// Use process.cwd() as the project root — works from both source (tsx) and compiled (dist/) contexts.
|
|
27
34
|
|
package/src/auth/Auth.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { authPrisma } from '../core/prisma';
|
|
|
5
5
|
import { ErrorResponse } from '../core/errors';
|
|
6
6
|
import { createStore, SessionStoreManager } from './stores';
|
|
7
7
|
import { loadDMMF, findUserModel, findIdentifierFields, findPasswordField } from '../core/dmmf';
|
|
8
|
-
import type { RapiddUser, AuthOptions,
|
|
8
|
+
import type { RapiddUser, AuthOptions, AuthMethod, ISessionStore } from '../types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Authentication class for user login, logout, and session management
|
|
@@ -26,7 +26,7 @@ import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../ty
|
|
|
26
26
|
export class Auth {
|
|
27
27
|
options: Required<Pick<AuthOptions, 'passwordField' | 'saltRounds'>> & AuthOptions & {
|
|
28
28
|
identifierFields: string[];
|
|
29
|
-
|
|
29
|
+
methods: AuthMethod[];
|
|
30
30
|
cookieName: string;
|
|
31
31
|
customHeaderName: string;
|
|
32
32
|
session: { ttl: number; store?: string };
|
|
@@ -68,8 +68,8 @@ export class Auth {
|
|
|
68
68
|
...options.jwt,
|
|
69
69
|
},
|
|
70
70
|
saltRounds: options.saltRounds || parseInt(process.env.AUTH_SALT_ROUNDS || '10', 10),
|
|
71
|
-
|
|
72
|
-
|| (process.env.
|
|
71
|
+
methods: options.methods
|
|
72
|
+
|| (process.env.AUTH_METHODS?.split(',').map(s => s.trim()) as AuthMethod[])
|
|
73
73
|
|| ['bearer'],
|
|
74
74
|
cookieName: options.cookieName || process.env.AUTH_COOKIE_NAME || 'token',
|
|
75
75
|
customHeaderName: options.customHeaderName || process.env.AUTH_CUSTOM_HEADER || 'X-Auth-Token',
|
package/src/index.ts
CHANGED
package/src/orm/Model.ts
CHANGED
|
@@ -177,6 +177,7 @@ class Model {
|
|
|
177
177
|
const field = this.fields[fieldName];
|
|
178
178
|
if (!field) return value;
|
|
179
179
|
if (field.type === 'Int') return parseInt(value, 10);
|
|
180
|
+
if (field.type === 'BigInt') return BigInt(value);
|
|
180
181
|
if (field.type === 'Float' || field.type === 'Decimal') return parseFloat(value);
|
|
181
182
|
if (field.type === 'Boolean') return value === 'true';
|
|
182
183
|
return value;
|
|
@@ -239,10 +240,13 @@ class Model {
|
|
|
239
240
|
sortBy = sortBy?.trim();
|
|
240
241
|
sortOrder = sortOrder?.trim();
|
|
241
242
|
|
|
242
|
-
// Validate sort field - fall back to default for composite PK names
|
|
243
|
+
// Validate sort field - fall back to default for composite PK names or missing 'id'
|
|
243
244
|
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
244
|
-
// If the sortBy is a composite key name (e.g., "email_companyId"), use first PK field
|
|
245
245
|
if (sortBy === this.primaryKey && this.isCompositePK) {
|
|
246
|
+
// Composite key name (e.g., "email_companyId") → use first PK field
|
|
247
|
+
sortBy = this.defaultSortField;
|
|
248
|
+
} else if (sortBy === 'id') {
|
|
249
|
+
// Model doesn't have an 'id' field → fall back to actual primary key
|
|
246
250
|
sortBy = this.defaultSortField;
|
|
247
251
|
} else {
|
|
248
252
|
throw new ErrorResponse(400, "invalid_sort_field", { sortBy, modelName: this.constructor.name });
|
package/src/orm/QueryBuilder.ts
CHANGED
|
@@ -501,10 +501,10 @@ class QueryBuilder {
|
|
|
501
501
|
/**
|
|
502
502
|
* Parse numeric filter operators
|
|
503
503
|
*/
|
|
504
|
-
#filterNumber(value: string): Record<string, number> | null {
|
|
504
|
+
#filterNumber(value: string): Record<string, number | bigint> | null {
|
|
505
505
|
const numOperators = ['lt:', 'lte:', 'gt:', 'gte:', 'eq:', 'ne:', 'between:'];
|
|
506
506
|
const foundOperator = numOperators.find((op: string) => value.startsWith(op));
|
|
507
|
-
let numValue: string
|
|
507
|
+
let numValue: string = value;
|
|
508
508
|
let prismaOp = 'equals';
|
|
509
509
|
|
|
510
510
|
if (foundOperator) {
|
|
@@ -517,19 +517,36 @@ class QueryBuilder {
|
|
|
517
517
|
case 'eq:': prismaOp = 'equals'; break;
|
|
518
518
|
case 'ne:': prismaOp = 'not'; break;
|
|
519
519
|
case 'between:': {
|
|
520
|
-
|
|
521
|
-
const [
|
|
522
|
-
|
|
520
|
+
const parts = numValue.split(';').map((v: string) => v.trim());
|
|
521
|
+
const [startStr, endStr] = parts;
|
|
522
|
+
const start = this.#parseNumericValue(startStr);
|
|
523
|
+
const end = this.#parseNumericValue(endStr);
|
|
524
|
+
if (start == null || end == null) return null;
|
|
523
525
|
return { gte: start, lte: end };
|
|
524
526
|
}
|
|
525
527
|
}
|
|
526
528
|
}
|
|
527
529
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (isNaN(numValue)) return null;
|
|
530
|
+
const parsed = this.#parseNumericValue(numValue);
|
|
531
|
+
if (parsed == null) return null;
|
|
531
532
|
|
|
532
|
-
return { [prismaOp]:
|
|
533
|
+
return { [prismaOp]: parsed };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Parse a numeric string, returning BigInt for integers beyond safe range
|
|
538
|
+
*/
|
|
539
|
+
#parseNumericValue(value: string): number | bigint | null {
|
|
540
|
+
if (value.includes('.')) {
|
|
541
|
+
const num = parseFloat(value);
|
|
542
|
+
return isNaN(num) ? null : num;
|
|
543
|
+
}
|
|
544
|
+
const num = Number(value);
|
|
545
|
+
if (isNaN(num)) return null;
|
|
546
|
+
if (!Number.isSafeInteger(num)) {
|
|
547
|
+
try { return BigInt(value); } catch { return null; }
|
|
548
|
+
}
|
|
549
|
+
return num;
|
|
533
550
|
}
|
|
534
551
|
|
|
535
552
|
/**
|
package/src/plugins/auth.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
2
3
|
import fp from 'fastify-plugin';
|
|
3
4
|
import { Auth } from '../auth/Auth';
|
|
4
5
|
import { ErrorResponse } from '../core/errors';
|
|
5
|
-
import type { RapiddUser, AuthOptions,
|
|
6
|
+
import type { RapiddUser, AuthOptions, AuthMethod, RouteAuthConfig } from '../types';
|
|
6
7
|
|
|
7
8
|
interface AuthPluginOptions {
|
|
8
9
|
auth?: Auth;
|
|
@@ -35,20 +36,54 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
|
|
39
|
+
// Load endpointAuthMethod from config/app.json (prefix → method(s) mapping)
|
|
40
|
+
let endpointAuthMethod: Record<string, AuthMethod | AuthMethod[] | null> = {};
|
|
41
|
+
try {
|
|
42
|
+
const appConfig = require(path.join(process.cwd(), 'config', 'app.json'));
|
|
43
|
+
if (appConfig.endpointAuthMethod) {
|
|
44
|
+
endpointAuthMethod = appConfig.endpointAuthMethod;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// No app.json or no endpointAuthMethod — use global default
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pre-sort prefixes by length (longest first) for correct matching
|
|
51
|
+
const sortedPrefixes = Object.keys(endpointAuthMethod)
|
|
52
|
+
.sort((a, b) => b.length - a.length);
|
|
53
|
+
|
|
54
|
+
// Parse auth on every request using configured methods (checked in order).
|
|
55
|
+
// Priority: route config > endpointAuthMethod prefix match > global default
|
|
40
56
|
fastify.addHook('onRequest', async (request) => {
|
|
41
57
|
const routeAuth = (request.routeOptions?.config as any)?.auth as RouteAuthConfig | undefined;
|
|
42
|
-
|
|
58
|
+
|
|
59
|
+
let methods: AuthMethod[];
|
|
60
|
+
if (routeAuth?.methods) {
|
|
61
|
+
methods = routeAuth.methods;
|
|
62
|
+
} else {
|
|
63
|
+
const matchedPrefix = sortedPrefixes.find(p => request.url.startsWith(p));
|
|
64
|
+
if (matchedPrefix) {
|
|
65
|
+
const value = endpointAuthMethod[matchedPrefix];
|
|
66
|
+
if (value === null) {
|
|
67
|
+
methods = auth.options.methods;
|
|
68
|
+
} else if (typeof value === 'string') {
|
|
69
|
+
methods = [value];
|
|
70
|
+
} else {
|
|
71
|
+
methods = value;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
methods = auth.options.methods;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
const cookieName = routeAuth?.cookieName || auth.options.cookieName;
|
|
44
79
|
const customHeaderName = routeAuth?.customHeaderName || auth.options.customHeaderName;
|
|
45
80
|
|
|
46
81
|
let user: RapiddUser | null = null;
|
|
47
82
|
|
|
48
|
-
for (const
|
|
83
|
+
for (const method of methods) {
|
|
49
84
|
if (user) break;
|
|
50
85
|
|
|
51
|
-
switch (
|
|
86
|
+
switch (method) {
|
|
52
87
|
case 'bearer': {
|
|
53
88
|
const h = request.headers.authorization;
|
|
54
89
|
if (h?.startsWith('Bearer ')) {
|
|
@@ -90,7 +125,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
90
125
|
fastify.post('/auth/login', async (request, reply) => {
|
|
91
126
|
const result = await auth.login(request.body as { user: string; password: string });
|
|
92
127
|
|
|
93
|
-
if (auth.options.
|
|
128
|
+
if (auth.options.methods.includes('cookie')) {
|
|
94
129
|
reply.setCookie(auth.options.cookieName, result.accessToken, {
|
|
95
130
|
path: '/',
|
|
96
131
|
httpOnly: true,
|
|
@@ -106,7 +141,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
106
141
|
fastify.post('/auth/logout', async (request, reply) => {
|
|
107
142
|
const result = await auth.logout(request.headers.authorization);
|
|
108
143
|
|
|
109
|
-
if (auth.options.
|
|
144
|
+
if (auth.options.methods.includes('cookie')) {
|
|
110
145
|
reply.clearCookie(auth.options.cookieName, { path: '/' });
|
|
111
146
|
}
|
|
112
147
|
|
package/src/types.ts
CHANGED
|
@@ -10,10 +10,10 @@ export interface RapiddUser {
|
|
|
10
10
|
[key: string]: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type AuthMethod = 'bearer' | 'basic' | 'cookie' | 'header';
|
|
14
14
|
|
|
15
15
|
export interface RouteAuthConfig {
|
|
16
|
-
|
|
16
|
+
methods?: AuthMethod[];
|
|
17
17
|
cookieName?: string;
|
|
18
18
|
customHeaderName?: string;
|
|
19
19
|
}
|
|
@@ -32,7 +32,7 @@ export interface AuthOptions {
|
|
|
32
32
|
refreshExpiry?: string;
|
|
33
33
|
};
|
|
34
34
|
saltRounds?: number;
|
|
35
|
-
|
|
35
|
+
methods?: AuthMethod[];
|
|
36
36
|
cookieName?: string;
|
|
37
37
|
customHeaderName?: string;
|
|
38
38
|
}
|