adapt-authoring-auth-local 2.5.0 → 2.6.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.
- package/lib/LocalAuthModule.js +43 -24
- package/package.json +2 -2
- package/tests/LocalAuthModule.spec.js +43 -5
package/lib/LocalAuthModule.js
CHANGED
|
@@ -113,10 +113,17 @@ class LocalAuthModule extends AbstractAuthModule {
|
|
|
113
113
|
async sendInvite (email) {
|
|
114
114
|
const mailer = await this.app.waitForModule('mailer')
|
|
115
115
|
if (!mailer.isEnabled) return
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
const appName = this.app.config.get('adapt-authoring-core.appName')
|
|
117
|
+
const t = (k) => this.app.lang.translate(undefined, `app.${k}`, { appName })
|
|
118
|
+
await this.createPasswordReset(email, {
|
|
119
|
+
subject: t('email_invite_subject'),
|
|
120
|
+
content: {
|
|
121
|
+
emblem: 'spark',
|
|
122
|
+
title: t('email_invite_title'),
|
|
123
|
+
body: t('email_invite_body'),
|
|
124
|
+
button: { label: t('email_invite_button') }
|
|
125
|
+
}
|
|
126
|
+
}, this.getConfig('inviteTokenLifespan'))
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
/**
|
|
@@ -173,11 +180,20 @@ class LocalAuthModule extends AbstractAuthModule {
|
|
|
173
180
|
|
|
174
181
|
await this.disavowUser({ userId: user._id, authType: this.type })
|
|
175
182
|
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const html = this.app.lang.translate(undefined, 'app.updateuserhtml')
|
|
183
|
+
const appName = this.app.config.get('adapt-authoring-core.appName')
|
|
184
|
+
const t = (k) => this.app.lang.translate(undefined, `app.${k}`, { appName })
|
|
179
185
|
try {
|
|
180
|
-
if (mailer.isEnabled)
|
|
186
|
+
if (mailer.isEnabled) {
|
|
187
|
+
await mailer.sendTemplated({
|
|
188
|
+
to: user.email,
|
|
189
|
+
subject: t('email_passwordupdated_subject'),
|
|
190
|
+
content: {
|
|
191
|
+
emblem: 'check',
|
|
192
|
+
title: t('email_passwordupdated_title'),
|
|
193
|
+
body: t('email_passwordupdated_body')
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
181
197
|
} catch (e) {
|
|
182
198
|
this.log('error', e)
|
|
183
199
|
}
|
|
@@ -186,14 +202,15 @@ class LocalAuthModule extends AbstractAuthModule {
|
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/**
|
|
189
|
-
* Creates a new password reset token and sends
|
|
205
|
+
* Creates a new password reset token and sends a templated email. The reset URL
|
|
206
|
+
* is built here and injected into the content's button.
|
|
190
207
|
* @param {String} email
|
|
191
|
-
* @param {
|
|
192
|
-
* @param {String}
|
|
193
|
-
* @param {
|
|
208
|
+
* @param {Object} message
|
|
209
|
+
* @param {String} message.subject Email subject (already translated)
|
|
210
|
+
* @param {Object} message.content Templated-email content ({ emblem?, title, body, button? }) — the button's url is set here
|
|
194
211
|
* @param {Number} lifespan The lifespan of the reset
|
|
195
212
|
*/
|
|
196
|
-
async createPasswordReset (email, subject,
|
|
213
|
+
async createPasswordReset (email, { subject, content } = {}, lifespan) {
|
|
197
214
|
if (!email) {
|
|
198
215
|
throw this.app.errors.INVALID_PARAMS.setData({ params: ['email'] })
|
|
199
216
|
}
|
|
@@ -204,13 +221,8 @@ class LocalAuthModule extends AbstractAuthModule {
|
|
|
204
221
|
const path = this.getConfig('resetUrl')
|
|
205
222
|
.replace(/{{token}}/g, token)
|
|
206
223
|
.replace(/{{email}}/g, email)
|
|
207
|
-
|
|
208
|
-
await mailer.
|
|
209
|
-
to: email,
|
|
210
|
-
subject,
|
|
211
|
-
text: textContent.replace(/{{url}}/g, url),
|
|
212
|
-
html: htmlContent.replace(/{{url}}/g, url)
|
|
213
|
-
})
|
|
224
|
+
if (content?.button) content.button.url = `${server.root.url}${path}`
|
|
225
|
+
await mailer.sendTemplated({ to: email, subject, content })
|
|
214
226
|
} catch (e) {
|
|
215
227
|
const cause = e?.data?.error ?? e
|
|
216
228
|
if (isSmtpConnectionError(e)) {
|
|
@@ -263,10 +275,17 @@ class LocalAuthModule extends AbstractAuthModule {
|
|
|
263
275
|
async forgotPasswordHandler (req, res, next) {
|
|
264
276
|
try {
|
|
265
277
|
const { email } = req.body
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
const appName = this.app.config.get('adapt-authoring-core.appName')
|
|
279
|
+
const t = (k) => this.app.lang.translate(undefined, `app.${k}`, { appName })
|
|
280
|
+
await this.createPasswordReset(email, {
|
|
281
|
+
subject: t('email_reset_subject'),
|
|
282
|
+
content: {
|
|
283
|
+
emblem: 'key',
|
|
284
|
+
title: t('email_reset_title'),
|
|
285
|
+
body: t('email_reset_body'),
|
|
286
|
+
button: { label: t('email_reset_button') }
|
|
287
|
+
}
|
|
288
|
+
})
|
|
270
289
|
this.log('debug', 'RESET_SENT', email, req?.auth?.user?._id?.toString())
|
|
271
290
|
} catch (e) { // don't return an error to avoid signifying correct user/pass combinations
|
|
272
291
|
this.log('error', 'RESET_PASS_FAILED', e)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-auth-local",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"adapt-authoring-jsonschema": "^1.2.0",
|
|
22
|
-
"adapt-authoring-mailer": "^1.0
|
|
22
|
+
"adapt-authoring-mailer": "^1.6.0",
|
|
23
23
|
"adapt-authoring-mongodb": "^3.0.0",
|
|
24
24
|
"adapt-authoring-roles": "^1.1.3",
|
|
25
25
|
"adapt-authoring-server": "^2.1.0",
|
|
@@ -46,6 +46,7 @@ let disavowCalls = []
|
|
|
46
46
|
let secureRouteCalls = []
|
|
47
47
|
let unsecureRouteCalls = []
|
|
48
48
|
let mailerSendCalls = []
|
|
49
|
+
let mailerSendTemplatedCalls = []
|
|
49
50
|
|
|
50
51
|
const mockUsers = {
|
|
51
52
|
find: async (query) => usersStore.filter(u => {
|
|
@@ -72,7 +73,8 @@ const mockRoles = {
|
|
|
72
73
|
|
|
73
74
|
const mockMailer = {
|
|
74
75
|
isEnabled: true,
|
|
75
|
-
send: async (opts) => { mailerSendCalls.push(opts) }
|
|
76
|
+
send: async (opts) => { mailerSendCalls.push(opts) },
|
|
77
|
+
sendTemplated: async (opts) => { mailerSendTemplatedCalls.push(opts) }
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
const mockServer = {
|
|
@@ -114,6 +116,7 @@ const mockApp = {
|
|
|
114
116
|
if (names.length === 1) return moduleMap[names[0]]
|
|
115
117
|
return names.map(n => moduleMap[n])
|
|
116
118
|
},
|
|
119
|
+
config: { get: () => 'Adapt' },
|
|
117
120
|
lang: {
|
|
118
121
|
translate: (_, key) => 'translated:' + key
|
|
119
122
|
}
|
|
@@ -570,10 +573,20 @@ describe('LocalAuthModule', () => {
|
|
|
570
573
|
})
|
|
571
574
|
|
|
572
575
|
it('should send email notification after password update when mailer is enabled', async () => {
|
|
573
|
-
|
|
576
|
+
mailerSendTemplatedCalls = []
|
|
574
577
|
await mod.updateUser('user-id-1', { password: 'newpassword' })
|
|
575
|
-
assert.ok(
|
|
576
|
-
assert.equal(
|
|
578
|
+
assert.ok(mailerSendTemplatedCalls.length > 0)
|
|
579
|
+
assert.equal(mailerSendTemplatedCalls[0].to, 'test@example.com')
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('should send a templated password-updated email (check emblem, no button)', async () => {
|
|
583
|
+
mailerSendTemplatedCalls = []
|
|
584
|
+
await mod.updateUser('user-id-1', { password: 'newpassword' })
|
|
585
|
+
const sent = mailerSendTemplatedCalls[0]
|
|
586
|
+
assert.equal(sent.content.emblem, 'check')
|
|
587
|
+
assert.ok(sent.subject)
|
|
588
|
+
assert.ok(sent.content.title)
|
|
589
|
+
assert.equal(sent.content.button, undefined)
|
|
577
590
|
})
|
|
578
591
|
|
|
579
592
|
it('should pass useDefaults:false and ignoreRequired:true for non-password updates', async () => {
|
|
@@ -581,6 +594,31 @@ describe('LocalAuthModule', () => {
|
|
|
581
594
|
})
|
|
582
595
|
})
|
|
583
596
|
|
|
597
|
+
describe('#sendInvite()', () => {
|
|
598
|
+
it('should build invite content (spark emblem + set-password button) for createPasswordReset', async () => {
|
|
599
|
+
let captured
|
|
600
|
+
const original = mod.createPasswordReset.bind(mod)
|
|
601
|
+
mod.createPasswordReset = async (email, message) => { captured = { email, message } }
|
|
602
|
+
await mod.sendInvite('invite@example.com')
|
|
603
|
+
mod.createPasswordReset = original
|
|
604
|
+
assert.equal(captured.email, 'invite@example.com')
|
|
605
|
+
assert.equal(captured.message.content.emblem, 'spark')
|
|
606
|
+
assert.ok(captured.message.subject)
|
|
607
|
+
assert.ok(captured.message.content.button.label)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('should be a no-op when the mailer is disabled', async () => {
|
|
611
|
+
mockMailer.isEnabled = false
|
|
612
|
+
let called = false
|
|
613
|
+
const original = mod.createPasswordReset.bind(mod)
|
|
614
|
+
mod.createPasswordReset = async () => { called = true }
|
|
615
|
+
await mod.sendInvite('invite@example.com')
|
|
616
|
+
mod.createPasswordReset = original
|
|
617
|
+
mockMailer.isEnabled = true
|
|
618
|
+
assert.equal(called, false)
|
|
619
|
+
})
|
|
620
|
+
})
|
|
621
|
+
|
|
584
622
|
describe('#createPasswordReset()', () => {
|
|
585
623
|
it('should throw when email is not provided', async () => {
|
|
586
624
|
await assert.rejects(
|
|
@@ -648,7 +686,7 @@ describe('LocalAuthModule', () => {
|
|
|
648
686
|
it('should pass inviteTokenLifespan to createPasswordReset', async () => {
|
|
649
687
|
let receivedLifespan
|
|
650
688
|
const original = mod.createPasswordReset.bind(mod)
|
|
651
|
-
mod.createPasswordReset = async (email,
|
|
689
|
+
mod.createPasswordReset = async (email, message, lifespan) => {
|
|
652
690
|
receivedLifespan = lifespan
|
|
653
691
|
}
|
|
654
692
|
const req = {
|