json-rest-api 1.0.10 → 1.0.11
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/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
package/tests/jwt-auth.test.js
DELETED
|
@@ -1,692 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import jwt from 'jsonwebtoken';
|
|
4
|
-
import knexLib from 'knex';
|
|
5
|
-
import express from 'express';
|
|
6
|
-
import request from 'supertest';
|
|
7
|
-
import {
|
|
8
|
-
validateJsonApiStructure,
|
|
9
|
-
cleanTables,
|
|
10
|
-
createJsonApiDocument,
|
|
11
|
-
createRelationship,
|
|
12
|
-
resourceIdentifier,
|
|
13
|
-
assertResourceAttributes
|
|
14
|
-
} from './helpers/test-utils.js';
|
|
15
|
-
import { createBasicApi } from './fixtures/api-configs.js';
|
|
16
|
-
import { JwtAuthPlugin } from '../plugins/core/jwt-auth-plugin.js';
|
|
17
|
-
import { ExpressPlugin } from '../plugins/core/connectors/express-plugin.js';
|
|
18
|
-
|
|
19
|
-
const TEST_SECRET = 'test-secret-key';
|
|
20
|
-
const TEST_USER = {
|
|
21
|
-
sub: '123',
|
|
22
|
-
email: 'test@example.com',
|
|
23
|
-
roles: ['user']
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function createToken(payload, options = {}) {
|
|
27
|
-
return jwt.sign(
|
|
28
|
-
{
|
|
29
|
-
...TEST_USER,
|
|
30
|
-
...payload,
|
|
31
|
-
jti: options.jti || `test-${Date.now()}`,
|
|
32
|
-
iat: Math.floor(Date.now() / 1000),
|
|
33
|
-
exp: Math.floor(Date.now() / 1000) + (options.expiresIn || 3600)
|
|
34
|
-
},
|
|
35
|
-
TEST_SECRET,
|
|
36
|
-
{ algorithm: 'HS256' }
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Create Knex instance for tests
|
|
41
|
-
const knex = knexLib({
|
|
42
|
-
client: 'better-sqlite3',
|
|
43
|
-
connection: {
|
|
44
|
-
filename: ':memory:'
|
|
45
|
-
},
|
|
46
|
-
useNullAsDefault: true
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// API instance and Express app that persist across tests
|
|
50
|
-
let api;
|
|
51
|
-
let app;
|
|
52
|
-
|
|
53
|
-
describe('JWT Auth Plugin', () => {
|
|
54
|
-
before(async () => {
|
|
55
|
-
// Initialize API once with Express
|
|
56
|
-
app = express();
|
|
57
|
-
api = await createBasicApi(knex, {
|
|
58
|
-
includeExpress: true,
|
|
59
|
-
express: { app }
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Install JWT auth plugin with all features enabled
|
|
63
|
-
await api.use(JwtAuthPlugin, {
|
|
64
|
-
secret: TEST_SECRET,
|
|
65
|
-
revocation: {
|
|
66
|
-
enabled: true,
|
|
67
|
-
storage: 'database'
|
|
68
|
-
},
|
|
69
|
-
endpoints: {
|
|
70
|
-
logout: '/auth/logout'
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Mount the routes on the Express app
|
|
75
|
-
api.http.express.mount(app);
|
|
76
|
-
|
|
77
|
-
// Add posts resource for declarative auth tests
|
|
78
|
-
await api.addResource('posts', {
|
|
79
|
-
schema: {
|
|
80
|
-
id: { type: 'id' },
|
|
81
|
-
title: { type: 'string', required: true },
|
|
82
|
-
content: { type: 'string' },
|
|
83
|
-
user_id: { type: 'string' },
|
|
84
|
-
published: { type: 'boolean', default: false }
|
|
85
|
-
},
|
|
86
|
-
auth: {
|
|
87
|
-
query: ['public'],
|
|
88
|
-
get: ['public'],
|
|
89
|
-
post: ['authenticated'],
|
|
90
|
-
patch: ['is_owner', 'has_role:editor', 'admin'],
|
|
91
|
-
delete: ['is_owner', 'admin']
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
await api.resources.posts.createKnexTable();
|
|
95
|
-
|
|
96
|
-
// Add moderated_posts resource for custom auth checker tests
|
|
97
|
-
await api.addResource('moderated_posts', {
|
|
98
|
-
schema: {
|
|
99
|
-
id: { type: 'id' },
|
|
100
|
-
title: { type: 'string', required: true },
|
|
101
|
-
flagged: { type: 'boolean', default: false }
|
|
102
|
-
},
|
|
103
|
-
auth: {
|
|
104
|
-
query: ['public'],
|
|
105
|
-
get: ['public'],
|
|
106
|
-
post: ['authenticated'],
|
|
107
|
-
patch: ['is_moderator']
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
await api.resources.moderated_posts.createKnexTable();
|
|
111
|
-
|
|
112
|
-
// Register the custom moderator checker
|
|
113
|
-
api.helpers.auth.registerChecker('is_moderator', (context) => {
|
|
114
|
-
return context.auth?.roles?.includes('moderator');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
after(async () => {
|
|
119
|
-
// Clean up JWT plugin resources
|
|
120
|
-
if (api.helpers?.auth?.cleanup) {
|
|
121
|
-
api.helpers.auth.cleanup();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Close database connection to allow tests to exit
|
|
125
|
-
await knex.destroy();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('Basic JWT Authentication', () => {
|
|
129
|
-
beforeEach(async () => {
|
|
130
|
-
// Clean all tables before each test
|
|
131
|
-
await cleanTables(knex, [
|
|
132
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
133
|
-
'revoked_tokens'
|
|
134
|
-
]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should populate context.auth for valid tokens', async () => {
|
|
138
|
-
const token = createToken();
|
|
139
|
-
|
|
140
|
-
// First test that the API works without auth
|
|
141
|
-
const testResponse = await request(app)
|
|
142
|
-
.get('/api/countries')
|
|
143
|
-
.set('Accept', 'application/vnd.api+json');
|
|
144
|
-
|
|
145
|
-
assert.equal(testResponse.status, 200);
|
|
146
|
-
assert.ok(testResponse.body.data);
|
|
147
|
-
assert.equal(testResponse.body.data.length, 0);
|
|
148
|
-
|
|
149
|
-
// Create a test country
|
|
150
|
-
const createResponse = await request(app)
|
|
151
|
-
.post('/api/countries')
|
|
152
|
-
.send({
|
|
153
|
-
data: {
|
|
154
|
-
type: 'countries',
|
|
155
|
-
attributes: { name: 'Test Country', code: 'TC' }
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
159
|
-
.set('Accept', 'application/vnd.api+json');
|
|
160
|
-
|
|
161
|
-
assert.equal(createResponse.status, 201);
|
|
162
|
-
|
|
163
|
-
// Now make authenticated request
|
|
164
|
-
const response = await request(app)
|
|
165
|
-
.get('/api/countries')
|
|
166
|
-
.set('Authorization', `Bearer ${token}`)
|
|
167
|
-
.set('Accept', 'application/vnd.api+json');
|
|
168
|
-
|
|
169
|
-
assert.equal(response.status, 200);
|
|
170
|
-
assert.ok(response.body.data);
|
|
171
|
-
assert.equal(response.body.data.length, 1);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('should not populate context.auth for expired tokens', async () => {
|
|
175
|
-
const token = createToken({}, { expiresIn: -1 }); // Already expired
|
|
176
|
-
|
|
177
|
-
// Try to access protected resource with expired token
|
|
178
|
-
const response = await request(app)
|
|
179
|
-
.get('/api/countries')
|
|
180
|
-
.set('Authorization', `Bearer ${token}`)
|
|
181
|
-
.set('Accept', 'application/vnd.api+json');
|
|
182
|
-
|
|
183
|
-
// Should still return 200 but without auth context
|
|
184
|
-
// (unless the resource requires authentication)
|
|
185
|
-
assert.equal(response.status, 200);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should not populate context.auth for invalid signatures', async () => {
|
|
189
|
-
const token = jwt.sign(TEST_USER, 'wrong-secret');
|
|
190
|
-
|
|
191
|
-
const response = await request(app)
|
|
192
|
-
.get('/api/countries')
|
|
193
|
-
.set('Authorization', `Bearer ${token}`)
|
|
194
|
-
.set('Accept', 'application/vnd.api+json');
|
|
195
|
-
|
|
196
|
-
// Should still return 200 but without auth context
|
|
197
|
-
assert.equal(response.status, 200);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should allow requests without tokens', async () => {
|
|
201
|
-
const response = await request(app)
|
|
202
|
-
.get('/api/countries')
|
|
203
|
-
.set('Accept', 'application/vnd.api+json');
|
|
204
|
-
|
|
205
|
-
assert.equal(response.status, 200);
|
|
206
|
-
assert.ok(response.body.data);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('JWT Helper Functions', () => {
|
|
211
|
-
it('should verify valid tokens', async () => {
|
|
212
|
-
const { verifyToken } = await import('../plugins/core/lib/jwt-auth-helpers.js');
|
|
213
|
-
const token = createToken();
|
|
214
|
-
|
|
215
|
-
const decoded = await verifyToken(token, {
|
|
216
|
-
secret: TEST_SECRET,
|
|
217
|
-
algorithms: ['HS256']
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
assert.equal(decoded.sub, '123');
|
|
221
|
-
assert.equal(decoded.email, 'test@example.com');
|
|
222
|
-
assert.deepEqual(decoded.roles, ['user']);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should reject expired tokens', async () => {
|
|
226
|
-
const { verifyToken } = await import('../plugins/core/lib/jwt-auth-helpers.js');
|
|
227
|
-
const token = createToken({}, { expiresIn: -1 });
|
|
228
|
-
|
|
229
|
-
await assert.rejects(
|
|
230
|
-
async () => {
|
|
231
|
-
await verifyToken(token, {
|
|
232
|
-
secret: TEST_SECRET,
|
|
233
|
-
algorithms: ['HS256']
|
|
234
|
-
});
|
|
235
|
-
},
|
|
236
|
-
{ name: 'TokenExpiredError' }
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should reject tokens with invalid signature', async () => {
|
|
241
|
-
const { verifyToken } = await import('../plugins/core/lib/jwt-auth-helpers.js');
|
|
242
|
-
const token = jwt.sign(TEST_USER, 'wrong-secret');
|
|
243
|
-
|
|
244
|
-
await assert.rejects(
|
|
245
|
-
async () => {
|
|
246
|
-
await verifyToken(token, {
|
|
247
|
-
secret: TEST_SECRET,
|
|
248
|
-
algorithms: ['HS256']
|
|
249
|
-
});
|
|
250
|
-
},
|
|
251
|
-
{ name: 'JsonWebTokenError' }
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('Token Revocation', () => {
|
|
257
|
-
beforeEach(async () => {
|
|
258
|
-
// Clean tables before each test
|
|
259
|
-
await cleanTables(knex, [
|
|
260
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
261
|
-
'revoked_tokens'
|
|
262
|
-
]);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('should revoke tokens on logout', async () => {
|
|
266
|
-
const jti = 'test-token-123';
|
|
267
|
-
const token = createToken({}, { jti });
|
|
268
|
-
|
|
269
|
-
// First, create some data with the token
|
|
270
|
-
await api.resources.countries.post({
|
|
271
|
-
data: {
|
|
272
|
-
type: 'countries',
|
|
273
|
-
attributes: { name: 'Test Country', code: 'TC' }
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Call logout endpoint
|
|
278
|
-
const logoutResponse = await request(app)
|
|
279
|
-
.post('/auth/logout')
|
|
280
|
-
.set('Authorization', `Bearer ${token}`)
|
|
281
|
-
.set('Accept', 'application/json');
|
|
282
|
-
|
|
283
|
-
assert.equal(logoutResponse.status, 200);
|
|
284
|
-
assert.equal(logoutResponse.body.success, true);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Try to use the token again - it should fail
|
|
288
|
-
const response = await request(app)
|
|
289
|
-
.get('/api/countries')
|
|
290
|
-
.set('Authorization', `Bearer ${token}`)
|
|
291
|
-
.set('Accept', 'application/vnd.api+json');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// The request should succeed but without auth context
|
|
295
|
-
assert.equal(response.status, 200);
|
|
296
|
-
|
|
297
|
-
// Check token is in revocation table
|
|
298
|
-
const revokedTokens = await knex('revoked_tokens').select();
|
|
299
|
-
assert.equal(revokedTokens.length, 1);
|
|
300
|
-
assert.equal(revokedTokens[0].jti, jti);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should not populate auth for revoked tokens', async () => {
|
|
304
|
-
const jti = 'test-token-456';
|
|
305
|
-
const token = createToken({}, { jti });
|
|
306
|
-
|
|
307
|
-
// Manually insert into revoked_tokens table
|
|
308
|
-
await knex('revoked_tokens').insert({
|
|
309
|
-
jti: jti,
|
|
310
|
-
user_id: '123',
|
|
311
|
-
expires_at: new Date(Date.now() + 3600000), // 1 hour from now
|
|
312
|
-
revoked_at: new Date()
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Try to use the revoked token
|
|
316
|
-
const response = await request(app)
|
|
317
|
-
.get('/api/countries')
|
|
318
|
-
.set('Authorization', `Bearer ${token}`)
|
|
319
|
-
.set('Accept', 'application/vnd.api+json');
|
|
320
|
-
|
|
321
|
-
// Request should succeed but without auth
|
|
322
|
-
assert.equal(response.status, 200);
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
describe('Declarative Auth System', () => {
|
|
327
|
-
let countryId;
|
|
328
|
-
let userId = '123';
|
|
329
|
-
let adminId = '456';
|
|
330
|
-
|
|
331
|
-
beforeEach(async () => {
|
|
332
|
-
// Clean tables before each test
|
|
333
|
-
await cleanTables(knex, [
|
|
334
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
335
|
-
'posts'
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
// Create test data
|
|
339
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'USA', code: 'US' });
|
|
340
|
-
const countryResult = await api.resources.countries.post({
|
|
341
|
-
inputRecord: countryDoc,
|
|
342
|
-
simplified: false
|
|
343
|
-
});
|
|
344
|
-
countryId = countryResult.data.id;
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('should allow public access to query', async () => {
|
|
348
|
-
// Create a post first
|
|
349
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
350
|
-
title: 'Test Post',
|
|
351
|
-
content: 'Test content',
|
|
352
|
-
user_id: userId,
|
|
353
|
-
published: true
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Need auth to create
|
|
357
|
-
const authContext = {
|
|
358
|
-
auth: { userId }
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
await assert.doesNotReject(async () => {
|
|
362
|
-
await api.resources.posts.post({
|
|
363
|
-
inputRecord: postDoc,
|
|
364
|
-
simplified: false
|
|
365
|
-
}, authContext);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// But can query without auth
|
|
369
|
-
const publicContext = { auth: null };
|
|
370
|
-
|
|
371
|
-
await assert.doesNotReject(async () => {
|
|
372
|
-
const posts = await api.resources.posts.query({
|
|
373
|
-
simplified: false
|
|
374
|
-
}, publicContext);
|
|
375
|
-
validateJsonApiStructure(posts, true);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('should require authentication for post creation', async () => {
|
|
380
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
381
|
-
title: 'Test Post',
|
|
382
|
-
content: 'Test content'
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// No auth context
|
|
386
|
-
const context = { auth: null };
|
|
387
|
-
|
|
388
|
-
await assert.rejects(
|
|
389
|
-
async () => {
|
|
390
|
-
await api.resources.posts.post({
|
|
391
|
-
inputRecord: postDoc,
|
|
392
|
-
simplified: false
|
|
393
|
-
}, context);
|
|
394
|
-
},
|
|
395
|
-
/Access denied.*Required one of: authenticated/
|
|
396
|
-
);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('should allow owner to update their post', async () => {
|
|
400
|
-
// Create post as user
|
|
401
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
402
|
-
title: 'My Post',
|
|
403
|
-
content: 'My content',
|
|
404
|
-
user_id: userId
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
const createContext = { auth: { userId } };
|
|
408
|
-
const postResult = await api.resources.posts.post({
|
|
409
|
-
inputRecord: postDoc,
|
|
410
|
-
simplified: false
|
|
411
|
-
}, createContext);
|
|
412
|
-
const postId = postResult.data.id;
|
|
413
|
-
|
|
414
|
-
// Update as owner
|
|
415
|
-
const updateDoc = createJsonApiDocument('posts', {
|
|
416
|
-
title: 'Updated Post'
|
|
417
|
-
});
|
|
418
|
-
updateDoc.data.id = postId;
|
|
419
|
-
|
|
420
|
-
await assert.doesNotReject(async () => {
|
|
421
|
-
await api.resources.posts.patch({
|
|
422
|
-
id: postId,
|
|
423
|
-
inputRecord: updateDoc,
|
|
424
|
-
simplified: false
|
|
425
|
-
}, createContext);
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should allow editor role to update any post', async () => {
|
|
430
|
-
// Create post as user
|
|
431
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
432
|
-
title: 'User Post',
|
|
433
|
-
content: 'User content',
|
|
434
|
-
user_id: userId
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
const userContext = { auth: { userId } };
|
|
438
|
-
const postResult = await api.resources.posts.post({
|
|
439
|
-
inputRecord: postDoc,
|
|
440
|
-
simplified: false
|
|
441
|
-
}, userContext);
|
|
442
|
-
const postId = postResult.data.id;
|
|
443
|
-
|
|
444
|
-
// Update as editor (different user)
|
|
445
|
-
const updateDoc = createJsonApiDocument('posts', {
|
|
446
|
-
title: 'Editor Updated'
|
|
447
|
-
});
|
|
448
|
-
updateDoc.data.id = postId;
|
|
449
|
-
|
|
450
|
-
const editorContext = {
|
|
451
|
-
auth: {
|
|
452
|
-
userId: 'editor-789',
|
|
453
|
-
roles: ['editor']
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
await assert.doesNotReject(async () => {
|
|
458
|
-
await api.resources.posts.patch({
|
|
459
|
-
id: postId,
|
|
460
|
-
inputRecord: updateDoc,
|
|
461
|
-
simplified: false
|
|
462
|
-
}, editorContext);
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('should allow admin to do anything', async () => {
|
|
467
|
-
// Create post as user
|
|
468
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
469
|
-
title: 'User Post',
|
|
470
|
-
content: 'User content',
|
|
471
|
-
user_id: userId
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
const userContext = { auth: { userId } };
|
|
475
|
-
const postResult = await api.resources.posts.post({
|
|
476
|
-
inputRecord: postDoc,
|
|
477
|
-
simplified: false
|
|
478
|
-
}, userContext);
|
|
479
|
-
const postId = postResult.data.id;
|
|
480
|
-
|
|
481
|
-
// Admin can update
|
|
482
|
-
const updateDoc = createJsonApiDocument('posts', {
|
|
483
|
-
title: 'Admin Updated'
|
|
484
|
-
});
|
|
485
|
-
updateDoc.data.id = postId;
|
|
486
|
-
|
|
487
|
-
const adminContext = {
|
|
488
|
-
auth: {
|
|
489
|
-
userId: adminId,
|
|
490
|
-
roles: ['admin']
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
await assert.doesNotReject(async () => {
|
|
495
|
-
await api.resources.posts.patch({
|
|
496
|
-
id: postId,
|
|
497
|
-
inputRecord: updateDoc,
|
|
498
|
-
simplified: false
|
|
499
|
-
}, adminContext);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Admin can delete
|
|
503
|
-
await assert.doesNotReject(async () => {
|
|
504
|
-
await api.resources.posts.delete({
|
|
505
|
-
id: postId,
|
|
506
|
-
simplified: false
|
|
507
|
-
}, adminContext);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it('should deny non-owner non-admin from updating', async () => {
|
|
512
|
-
// Create post as user
|
|
513
|
-
const postDoc = createJsonApiDocument('posts', {
|
|
514
|
-
title: 'User Post',
|
|
515
|
-
content: 'User content',
|
|
516
|
-
user_id: userId
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
const userContext = { auth: { userId } };
|
|
520
|
-
const postResult = await api.resources.posts.post({
|
|
521
|
-
inputRecord: postDoc,
|
|
522
|
-
simplified: false
|
|
523
|
-
}, userContext);
|
|
524
|
-
const postId = postResult.data.id;
|
|
525
|
-
|
|
526
|
-
// Try to update as different user without special roles
|
|
527
|
-
const updateDoc = createJsonApiDocument('posts', {
|
|
528
|
-
title: 'Hacker Updated'
|
|
529
|
-
});
|
|
530
|
-
updateDoc.data.id = postId;
|
|
531
|
-
|
|
532
|
-
const otherUserContext = {
|
|
533
|
-
auth: {
|
|
534
|
-
userId: 'other-999',
|
|
535
|
-
roles: ['user']
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
await assert.rejects(
|
|
540
|
-
async () => {
|
|
541
|
-
await api.resources.posts.patch({
|
|
542
|
-
id: postId,
|
|
543
|
-
inputRecord: updateDoc,
|
|
544
|
-
simplified: false
|
|
545
|
-
}, otherUserContext);
|
|
546
|
-
},
|
|
547
|
-
/Access denied.*Required one of: is_owner, has_role:editor, admin/
|
|
548
|
-
);
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
describe('Auth Helpers', () => {
|
|
553
|
-
beforeEach(async () => {
|
|
554
|
-
// No need to clean tables for helper tests
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it('should check authentication with requireAuth', () => {
|
|
558
|
-
const helpers = api.helpers;
|
|
559
|
-
|
|
560
|
-
// Not authenticated
|
|
561
|
-
assert.throws(() => {
|
|
562
|
-
helpers.auth.requireAuth({ auth: null });
|
|
563
|
-
}, /Authentication required/);
|
|
564
|
-
|
|
565
|
-
// Authenticated
|
|
566
|
-
const auth = helpers.auth.requireAuth({
|
|
567
|
-
auth: { userId: '123' }
|
|
568
|
-
});
|
|
569
|
-
assert.equal(auth.userId, '123');
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('should check roles with requireRoles', () => {
|
|
573
|
-
const helpers = api.helpers;
|
|
574
|
-
const context = {
|
|
575
|
-
auth: {
|
|
576
|
-
userId: '123',
|
|
577
|
-
roles: ['user', 'editor']
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
// Has required role
|
|
582
|
-
assert.doesNotThrow(() => {
|
|
583
|
-
helpers.auth.requireRoles(context, ['editor']);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
// Missing required role
|
|
587
|
-
assert.throws(() => {
|
|
588
|
-
helpers.auth.requireRoles(context, ['admin']);
|
|
589
|
-
}, /Required role/);
|
|
590
|
-
|
|
591
|
-
// At least one role matches
|
|
592
|
-
assert.doesNotThrow(() => {
|
|
593
|
-
helpers.auth.requireRoles(context, ['admin', 'editor']);
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
it('should check ownership with various inputs', () => {
|
|
598
|
-
const helpers = api.helpers;
|
|
599
|
-
const context = {
|
|
600
|
-
auth: {
|
|
601
|
-
userId: '123',
|
|
602
|
-
roles: ['user']
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
// With direct user ID
|
|
607
|
-
assert.doesNotThrow(() => {
|
|
608
|
-
helpers.auth.requireOwnership(context, '123');
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
assert.throws(() => {
|
|
612
|
-
helpers.auth.requireOwnership(context, '456');
|
|
613
|
-
}, /Access denied/);
|
|
614
|
-
|
|
615
|
-
// With record object
|
|
616
|
-
const ownedRecord = { id: '1', user_id: '123', title: 'My Post' };
|
|
617
|
-
assert.doesNotThrow(() => {
|
|
618
|
-
helpers.auth.requireOwnership(context, ownedRecord);
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
const otherRecord = { id: '2', user_id: '456', title: 'Other Post' };
|
|
622
|
-
assert.throws(() => {
|
|
623
|
-
helpers.auth.requireOwnership(context, otherRecord);
|
|
624
|
-
}, /Access denied/);
|
|
625
|
-
|
|
626
|
-
// Admin can access any resource
|
|
627
|
-
const adminContext = {
|
|
628
|
-
auth: {
|
|
629
|
-
userId: '999',
|
|
630
|
-
roles: ['admin']
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
assert.doesNotThrow(() => {
|
|
634
|
-
helpers.auth.requireOwnership(adminContext, otherRecord);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
describe('Custom Auth Checkers', () => {
|
|
640
|
-
beforeEach(async () => {
|
|
641
|
-
// Clean tables before each test
|
|
642
|
-
await cleanTables(knex, [
|
|
643
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
644
|
-
'moderated_posts'
|
|
645
|
-
]);
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
it('should allow registering custom auth checkers', async () => {
|
|
649
|
-
// The custom checker was already registered in the before() hook
|
|
650
|
-
// and the moderated_posts resource was already created
|
|
651
|
-
|
|
652
|
-
// Create a post
|
|
653
|
-
const postDoc = createJsonApiDocument('moderated_posts', {
|
|
654
|
-
title: 'Test Post',
|
|
655
|
-
flagged: true
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
const createContext = { auth: { userId: '123' } };
|
|
659
|
-
const postResult = await api.resources.moderated_posts.post({
|
|
660
|
-
inputRecord: postDoc,
|
|
661
|
-
simplified: false
|
|
662
|
-
}, createContext);
|
|
663
|
-
const postId = postResult.data.id;
|
|
664
|
-
|
|
665
|
-
// Try to update without moderator role
|
|
666
|
-
const updateDoc = createJsonApiDocument('moderated_posts', {
|
|
667
|
-
flagged: false
|
|
668
|
-
});
|
|
669
|
-
updateDoc.data.id = postId;
|
|
670
|
-
|
|
671
|
-
await assert.rejects(
|
|
672
|
-
async () => {
|
|
673
|
-
await api.resources.moderated_posts.patch({
|
|
674
|
-
id: postId,
|
|
675
|
-
inputRecord: updateDoc,
|
|
676
|
-
simplified: false
|
|
677
|
-
}, { auth: { userId: '123', roles: ['user'] } });
|
|
678
|
-
},
|
|
679
|
-
/Access denied.*Required one of: is_moderator/
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
// Update with moderator role should work
|
|
683
|
-
await assert.doesNotReject(async () => {
|
|
684
|
-
await api.resources.moderated_posts.patch({
|
|
685
|
-
id: postId,
|
|
686
|
-
inputRecord: updateDoc,
|
|
687
|
-
simplified: false
|
|
688
|
-
}, { auth: { userId: '456', roles: ['moderator'] } });
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
});
|