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.
@@ -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 subject = this.app.lang.translate(undefined, 'app.invitepasswordsubject')
117
- const text = this.app.lang.translate(undefined, 'app.invitepasswordtext')
118
- const html = this.app.lang.translate(undefined, 'app.invitepasswordhtml')
119
- await this.createPasswordReset(email, subject, text, html, this.getConfig('inviteTokenLifespan'))
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 subject = this.app.lang.translate(undefined, 'app.updateusersubject')
177
- const text = this.app.lang.translate(undefined, 'app.updateusertext')
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) await mailer.send({ to: user.email, subject, text, html })
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 an email
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 {String} subject
192
- * @param {String} textContent
193
- * @param {String} htmlContent
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, textContent, htmlContent, lifespan) {
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
- const url = `${server.root.url}${path}`
208
- await mailer.send({
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 subject = req.translate('app.forgotpasswordsubject')
267
- const text = req.translate('app.forgotpasswordtext')
268
- const html = req.translate('app.forgotpasswordhtml')
269
- await this.createPasswordReset(email, subject, text, html)
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.5.0",
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.2",
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
- mailerSendCalls = []
576
+ mailerSendTemplatedCalls = []
574
577
  await mod.updateUser('user-id-1', { password: 'newpassword' })
575
- assert.ok(mailerSendCalls.length > 0)
576
- assert.equal(mailerSendCalls[0].to, 'test@example.com')
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, subject, text, html, lifespan) => {
689
+ mod.createPasswordReset = async (email, message, lifespan) => {
652
690
  receivedLifespan = lifespan
653
691
  }
654
692
  const req = {