adapt-authoring-auth-local 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  import _ from 'lodash'
2
2
  import { AbstractAuthModule } from 'adapt-authoring-auth'
3
- import apidefs from './apidefs.js'
4
3
  import { compare, getRandomHex, validate } from './utils.js'
5
4
  import { formatDistanceToNowStrict as toNow } from 'date-fns'
6
5
  import PasswordUtils from './PasswordUtils.js'
@@ -20,52 +19,18 @@ class LocalAuthModule extends AbstractAuthModule {
20
19
 
21
20
  /** @override */
22
21
  async setValues () {
22
+ await super.setValues()
23
23
  /** @ignore */ this.userSchema = 'localauthuser'
24
- /** @ignore */ this.type = 'local'
25
- /** @ignore */ this.routes = [
26
- {
27
- route: '/invite',
28
- handlers: { post: this.inviteHandler.bind(this) },
29
- meta: apidefs.invite
30
- }, {
31
- route: '/registersuper',
32
- internal: true,
33
- handlers: { post: this.registerSuperHandler.bind(this) },
34
- meta: apidefs.registersuper
35
- }, {
36
- route: '/changepass',
37
- handlers: { post: this.changePasswordHandler.bind(this) },
38
- meta: apidefs.changepass
39
- }, {
40
- route: '/forgotpass',
41
- handlers: { post: this.forgotPasswordHandler.bind(this) },
42
- meta: apidefs.forgotpass
43
- }, {
44
- route: '/validatepass',
45
- handlers: { post: this.validatePasswordHandler.bind(this) },
46
- meta: apidefs.validatepass
47
- }
48
- ]
49
24
  }
50
25
 
51
26
  /** @override */
52
27
  async init () {
53
28
  await super.init()
54
- this.secureRoute('/invite', 'post', ['register:users'])
55
- this.secureRoute('/validatepass', 'post', ['read:me'])
56
- this.unsecureRoute('/registersuper', 'post')
57
- this.unsecureRoute('/changepass', 'post')
58
- this.unsecureRoute('/forgotpass', 'post')
59
- // add API metadata
60
- this.router.routes.find(r => r.route === '/').meta = apidefs.root
61
- this.router.routes.find(r => r.route === '/register').meta = apidefs.register
62
-
63
- const users = await this.app.waitForModule('users')
64
29
  /**
65
30
  * Local reference to the current UsersModule instance for convenience
66
31
  * @type {UsersModule}
67
32
  */
68
- this.users = users
33
+ this.users = await this.app.waitForModule('users')
69
34
  }
70
35
 
71
36
  /** @override */
@@ -1,7 +1,7 @@
1
1
  import { App } from 'adapt-authoring-core'
2
2
  import bcrypt from 'bcryptjs'
3
3
  import { compare, getRandomHex, validate } from './utils.js'
4
- import { promisify } from 'util'
4
+ import { promisify } from 'node:util'
5
5
 
6
6
  /** @ignore */ const passwordResetsCollectionName = 'passwordresets'
7
7
  /**
@@ -1,6 +1,6 @@
1
1
  import { App } from 'adapt-authoring-core'
2
2
  import bcrypt from 'bcryptjs'
3
- import { promisify } from 'util'
3
+ import { promisify } from 'node:util'
4
4
 
5
5
  /**
6
6
  * Compares a plain password to a hash
@@ -1,5 +1,5 @@
1
- import crypto from 'crypto'
2
- import { promisify } from 'util'
1
+ import crypto from 'node:crypto'
2
+ import { promisify } from 'node:util'
3
3
 
4
4
  /**
5
5
  * Creates a random hex string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-auth-local",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Module which implements username/password (local) authentication",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-auth-local",
6
6
  "license": "GPL-3.0",
@@ -22,7 +22,7 @@
22
22
  "adapt-authoring-mailer": "^1.0.2",
23
23
  "adapt-authoring-mongodb": "^3.0.0",
24
24
  "adapt-authoring-roles": "^1.1.3",
25
- "adapt-authoring-server": "^2.0.0",
25
+ "adapt-authoring-server": "^2.1.0",
26
26
  "adapt-authoring-users": "^1.0.2"
27
27
  },
28
28
  "devDependencies": {
package/routes.json ADDED
@@ -0,0 +1,234 @@
1
+ {
2
+ "type": "local",
3
+ "routes": [
4
+ {
5
+ "route": "/",
6
+ "override": true,
7
+ "handlers": { "post": "authenticateHandler" },
8
+ "meta": {
9
+ "post": {
10
+ "summary": "Authenticate with the API",
11
+ "requestBody": {
12
+ "content": {
13
+ "application/json": {
14
+ "schema": {
15
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
16
+ "type": "object",
17
+ "properties": {
18
+ "email": { "type": "string", "required": true },
19
+ "password": { "type": "string", "required": true },
20
+ "persistSession": { "type": "boolean" }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ },
26
+ "responses": { "204": {} }
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "route": "/register",
32
+ "override": true,
33
+ "handlers": { "post": "registerHandler" },
34
+ "meta": {
35
+ "post": {
36
+ "summary": "Register a new user",
37
+ "requestBody": {
38
+ "content": {
39
+ "application/json": {
40
+ "schema": {
41
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
42
+ "type": "object",
43
+ "properties": {
44
+ "email": { "type": "string", "required": true },
45
+ "firstName": { "type": "string", "required": true },
46
+ "lastName": { "type": "string", "required": true },
47
+ "password": { "type": "string", "required": true },
48
+ "roles": {
49
+ "type": "array",
50
+ "items": { "type": "string" }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "responses": {
58
+ "200": {
59
+ "content": {
60
+ "application/json": {
61
+ "schema": { "$ref": "#components/schemas/localauthuser" }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ },
69
+ {
70
+ "route": "/invite",
71
+ "handlers": { "post": "inviteHandler" },
72
+ "permissions": { "post": ["register:users"] },
73
+ "meta": {
74
+ "post": {
75
+ "summary": "Invite a new user",
76
+ "requestBody": {
77
+ "content": {
78
+ "application/json": {
79
+ "schema": {
80
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
81
+ "type": "object",
82
+ "properties": {
83
+ "email": { "type": "string" }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ },
89
+ "responses": { "204": {} }
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "route": "/registersuper",
95
+ "internal": true,
96
+ "handlers": { "post": "registerSuperHandler" },
97
+ "permissions": { "post": null },
98
+ "meta": {
99
+ "post": {
100
+ "summary": "Register a new super user",
101
+ "description": "Only one user can be registered in this way, and if a super user already exists the request will fail.",
102
+ "requestBody": {
103
+ "content": {
104
+ "application/json": {
105
+ "schema": {
106
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
107
+ "type": "object",
108
+ "properties": {
109
+ "email": { "type": "string", "required": true },
110
+ "password": { "type": "string", "required": true }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ },
116
+ "responses": {
117
+ "200": {
118
+ "content": {
119
+ "application/json": {
120
+ "schema": { "$ref": "#components/schemas/localauthuser" }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ },
128
+ {
129
+ "route": "/changepass",
130
+ "handlers": { "post": "changePasswordHandler" },
131
+ "permissions": { "post": null },
132
+ "meta": {
133
+ "post": {
134
+ "summary": "Change the password of a user",
135
+ "description": "Can be used with or without authentication. If authenticated, an email/password combination will be acepted. If unauthenticated, a valid reset token and password must be specified.",
136
+ "requestBody": {
137
+ "content": {
138
+ "application/json": {
139
+ "schema": {
140
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
141
+ "type": "object",
142
+ "properties": {
143
+ "email": { "type": "string" },
144
+ "password": { "type": "string" },
145
+ "token": { "type": "string" }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ },
151
+ "responses": { "204": {} }
152
+ }
153
+ }
154
+ },
155
+ {
156
+ "route": "/forgotpass",
157
+ "handlers": { "post": "forgotPasswordHandler" },
158
+ "permissions": { "post": null },
159
+ "meta": {
160
+ "post": {
161
+ "summary": "Trigger a password reset",
162
+ "description": "Generates a password reset and emails this to the user with instructions on updating their password.",
163
+ "requestBody": {
164
+ "content": {
165
+ "application/json": {
166
+ "schema": {
167
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
168
+ "type": "object",
169
+ "properties": {
170
+ "email": { "type": "string" }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ },
176
+ "responses": {
177
+ "200": {
178
+ "content": {
179
+ "application/json": {
180
+ "schema": {
181
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
182
+ "type": "object",
183
+ "properties": {
184
+ "message": { "type": "string" }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ },
194
+ {
195
+ "route": "/validatepass",
196
+ "handlers": { "post": "validatePasswordHandler" },
197
+ "permissions": { "post": ["read:me"] },
198
+ "meta": {
199
+ "post": {
200
+ "summary": "Validate password",
201
+ "description": "Checks that a password passes the required complexity specified in the application's configuration settings.",
202
+ "requestBody": {
203
+ "content": {
204
+ "application/json": {
205
+ "schema": {
206
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
207
+ "type": "object",
208
+ "properties": {
209
+ "password": { "type": "string", "required": true }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ },
215
+ "responses": {
216
+ "200": {
217
+ "content": {
218
+ "application/json": {
219
+ "schema": {
220
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
221
+ "type": "object",
222
+ "properties": {
223
+ "message": { "type": "string" }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ ]
234
+ }
@@ -122,6 +122,12 @@ mock.module('adapt-authoring-auth', {
122
122
 
123
123
  getConfig (key) { return authlocalConfig[key] }
124
124
 
125
+ async setValues () {
126
+ this.type = undefined
127
+ this.routes = undefined
128
+ this.userSchema = 'user'
129
+ }
130
+
125
131
  async init () {}
126
132
 
127
133
  async register (data) { return { _id: 'new-user-id', ...data } }
@@ -215,111 +221,23 @@ describe('LocalAuthModule', () => {
215
221
  assert.equal(instance.userSchema, 'localauthuser')
216
222
  })
217
223
 
218
- it('should set type to local', async () => {
219
- const instance = new LocalAuthModule()
220
- await instance.setValues()
221
- assert.equal(instance.type, 'local')
222
- })
223
-
224
- it('should define 5 routes', async () => {
225
- const instance = new LocalAuthModule()
226
- await instance.setValues()
227
- assert.equal(instance.routes.length, 5)
228
- })
229
-
230
- it('should include the expected route paths', async () => {
231
- const instance = new LocalAuthModule()
232
- await instance.setValues()
233
- const paths = instance.routes.map(r => r.route)
234
- assert.deepEqual(paths, ['/invite', '/registersuper', '/changepass', '/forgotpass', '/validatepass'])
235
- })
236
-
237
- it('should mark registersuper as internal', async () => {
224
+ it('should call super.setValues()', async () => {
238
225
  const instance = new LocalAuthModule()
239
226
  await instance.setValues()
240
- const superRoute = instance.routes.find(r => r.route === '/registersuper')
241
- assert.equal(superRoute.internal, true)
242
- })
243
-
244
- it('should define post handlers for all routes', async () => {
245
- const instance = new LocalAuthModule()
246
- await instance.setValues()
247
- for (const route of instance.routes) {
248
- assert.equal(typeof route.handlers.post, 'function')
249
- }
250
- })
251
-
252
- it('should define meta for all routes', async () => {
253
- const instance = new LocalAuthModule()
254
- await instance.setValues()
255
- for (const route of instance.routes) {
256
- assert.ok(route.meta)
257
- }
227
+ // super.setValues() initialises type and routes to undefined;
228
+ // in production, loadRouteConfig fills them from routes.json
229
+ assert.equal(instance.type, undefined)
230
+ assert.equal(instance.routes, undefined)
258
231
  })
259
232
  })
260
233
 
261
234
  describe('#init()', () => {
262
- it('should secure the invite route', async () => {
263
- const instance = new LocalAuthModule()
264
- instance.app = mockApp
265
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
266
- await instance.setValues()
267
- await instance.init()
268
- assert.ok(secureRouteCalls.some(c => c[0] === '/invite' && c[1] === 'post'))
269
- })
270
-
271
- it('should secure the validatepass route with read:me permission', async () => {
272
- const instance = new LocalAuthModule()
273
- instance.app = mockApp
274
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
275
- await instance.setValues()
276
- await instance.init()
277
- const call = secureRouteCalls.find(c => c[0] === '/validatepass')
278
- assert.ok(call)
279
- assert.deepEqual(call[2], ['read:me'])
280
- })
281
-
282
- it('should secure the invite route with register:users permission', async () => {
283
- const instance = new LocalAuthModule()
284
- instance.app = mockApp
285
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
286
- await instance.setValues()
287
- await instance.init()
288
- const call = secureRouteCalls.find(c => c[0] === '/invite')
289
- assert.ok(call)
290
- assert.deepEqual(call[2], ['register:users'])
291
- })
292
-
293
- it('should unsecure registersuper, changepass, and forgotpass routes', async () => {
294
- const instance = new LocalAuthModule()
295
- instance.app = mockApp
296
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
297
- await instance.setValues()
298
- await instance.init()
299
- const unsecuredPaths = unsecureRouteCalls.map(c => c[0])
300
- assert.ok(unsecuredPaths.includes('/registersuper'))
301
- assert.ok(unsecuredPaths.includes('/changepass'))
302
- assert.ok(unsecuredPaths.includes('/forgotpass'))
303
- })
304
-
305
235
  it('should set the users property', async () => {
306
236
  const instance = new LocalAuthModule()
307
237
  instance.app = mockApp
308
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
309
- await instance.setValues()
310
238
  await instance.init()
311
239
  assert.equal(instance.users, mockUsers)
312
240
  })
313
-
314
- it('should set meta on root and register routes', async () => {
315
- const instance = new LocalAuthModule()
316
- instance.app = mockApp
317
- instance.router = { routes: [{ route: '/', meta: null }, { route: '/register', meta: null }] }
318
- await instance.setValues()
319
- await instance.init()
320
- assert.ok(instance.router.routes.find(r => r.route === '/').meta)
321
- assert.ok(instance.router.routes.find(r => r.route === '/register').meta)
322
- })
323
241
  })
324
242
 
325
243
  describe('#authenticate()', () => {
package/lib/apidefs.js DELETED
@@ -1,195 +0,0 @@
1
- export default {
2
- changepass: {
3
- post: {
4
- summary: 'Change the password of a user',
5
- description: 'Can be used with or without authentication. If authenticated, an email/password combination will be acepted. If unauthenticated, a valid reset token and password must be specified.',
6
- requestBody: {
7
- content: {
8
- 'application/json': {
9
- schema: {
10
- $schema: 'https://json-schema.org/draft/2020-12/schema',
11
- type: 'object',
12
- properties: {
13
- email: { type: 'string' },
14
- password: { type: 'string' },
15
- token: { type: 'string' }
16
- }
17
- }
18
- }
19
- }
20
- },
21
- responses: { 204: {} }
22
- }
23
- },
24
- forgotpass: {
25
- post: {
26
- summary: 'Trigger a password reset',
27
- description: 'Generates a password reset and emails this to the user with instructions on updating their password.',
28
- requestBody: {
29
- content: {
30
- 'application/json': {
31
- schema: {
32
- $schema: 'https://json-schema.org/draft/2020-12/schema',
33
- type: 'object',
34
- properties: {
35
- email: { type: 'string' }
36
- }
37
- }
38
- }
39
- }
40
- },
41
- responses: {
42
- 200: {
43
- content: {
44
- 'application/json': {
45
- schema: {
46
- $schema: 'https://json-schema.org/draft/2020-12/schema',
47
- type: 'object',
48
- properties: {
49
- message: { type: 'string' }
50
- }
51
- }
52
- }
53
- }
54
- }
55
- }
56
- }
57
- },
58
- invite: {
59
- post: {
60
- summary: 'Invite a new user',
61
- requestBody: {
62
- content: {
63
- 'application/json': {
64
- schema: {
65
- $schema: 'https://json-schema.org/draft/2020-12/schema',
66
- type: 'object',
67
- properties: {
68
- email: { type: 'string' }
69
- }
70
- }
71
- }
72
- }
73
- },
74
- responses: { 204: {} }
75
- }
76
- },
77
- register: {
78
- post: {
79
- summary: 'Register a new user',
80
- requestBody: {
81
- content: {
82
- 'application/json': {
83
- schema: {
84
- $schema: 'https://json-schema.org/draft/2020-12/schema',
85
- type: 'object',
86
- properties: {
87
- email: { type: 'string', required: true },
88
- firstName: { type: 'string', required: true },
89
- lastName: { type: 'string', required: true },
90
- password: { type: 'string', required: true },
91
- roles: {
92
- type: 'array',
93
- items: { type: 'string' }
94
- }
95
- }
96
- }
97
- }
98
- }
99
- },
100
- responses: {
101
- 200: {
102
- content: {
103
- 'application/json': {
104
- schema: { $ref: '#components/schemas/localauthuser' }
105
- }
106
- }
107
- }
108
- }
109
- }
110
- },
111
- registersuper: {
112
- post: {
113
- summary: 'Register a new super user',
114
- description: 'Only one user can be registered in this way, and if a super user already exists the request will fail.',
115
- requestBody: {
116
- content: {
117
- 'application/json': {
118
- schema: {
119
- $schema: 'https://json-schema.org/draft/2020-12/schema',
120
- type: 'object',
121
- properties: {
122
- email: { type: 'string', required: true },
123
- password: { type: 'string', required: true }
124
- }
125
- }
126
- }
127
- }
128
- },
129
- responses: {
130
- 200: {
131
- content: {
132
- 'application/json': {
133
- schema: { $ref: '#components/schemas/localauthuser' }
134
- }
135
- }
136
- }
137
- }
138
- }
139
- },
140
- root: {
141
- post: {
142
- summary: 'Authenticate with the API',
143
- requestBody: {
144
- content: {
145
- 'application/json': {
146
- schema: {
147
- $schema: 'https://json-schema.org/draft/2020-12/schema',
148
- type: 'object',
149
- properties: {
150
- email: { type: 'string', required: true },
151
- password: { type: 'string', required: true },
152
- persistSession: { type: 'boolean' }
153
- }
154
- }
155
- }
156
- }
157
- },
158
- responses: { 204: {} }
159
- }
160
- },
161
- validatepass: {
162
- post: {
163
- summary: 'Validate password',
164
- description: 'Checks that a password passes the required complexity specified in the application\'s configuration settings.',
165
- requestBody: {
166
- content: {
167
- 'application/json': {
168
- schema: {
169
- $schema: 'https://json-schema.org/draft/2020-12/schema',
170
- type: 'object',
171
- properties: {
172
- password: { type: 'string', required: true }
173
- }
174
- }
175
- }
176
- }
177
- },
178
- responses: {
179
- 200: {
180
- content: {
181
- 'application/json': {
182
- schema: {
183
- $schema: 'https://json-schema.org/draft/2020-12/schema',
184
- type: 'object',
185
- properties: {
186
- message: { type: 'string' }
187
- }
188
- }
189
- }
190
- }
191
- }
192
- }
193
- }
194
- }
195
- }