json-rest-api 1.0.10 → 1.0.13

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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -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
- });