sumba 2.4.1 → 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.
Files changed (26) hide show
  1. package/extend/bajo/hook/dobo.sumba-contact-form@after-create-record.js +8 -8
  2. package/extend/bajo/hook/dobo.sumba-user@after-create-record.js +7 -6
  3. package/extend/bajo/hook/dobo.sumba-user@after-update-record.js +14 -12
  4. package/extend/bajoTemplate/layout/email.html +17 -0
  5. package/extend/bajoTemplate/layout/email.id.html +18 -0
  6. package/extend/bajoTemplate/partial/_mail/help-contact-form.html +1 -1
  7. package/extend/bajoTemplate/template/_mail/help-contact-form.html +1 -1
  8. package/extend/bajoTemplate/template/_mail/mystuff-change-password.html +1 -1
  9. package/extend/bajoTemplate/template/_mail/mystuff-reset-api-key.html +1 -1
  10. package/extend/bajoTemplate/template/_mail/user-activation-success.html +1 -1
  11. package/extend/bajoTemplate/template/_mail/user-forgot-password-changed.html +1 -1
  12. package/extend/bajoTemplate/template/_mail/user-forgot-password-link.html +1 -1
  13. package/extend/bajoTemplate/template/_mail/user-signup-success-active.html +1 -1
  14. package/extend/bajoTemplate/template/_mail/user-signup-success.html +1 -1
  15. package/extend/dobo/model/contact-form.json +1 -1
  16. package/extend/waibuMpa/route/help/contact-form.js +1 -1
  17. package/extend/waibuMpa/route/user/activation.js +3 -1
  18. package/extend/waibuMpa/route/user/forgot-password/@fpl.js +11 -9
  19. package/extend/waibuMpa/route/user/forgot-password.js +3 -3
  20. package/extend/waibuMpa/route/user/signup.js +1 -1
  21. package/extend/waibuMpa/route/your-stuff/change-password.js +3 -1
  22. package/extend/waibuMpa/route/your-stuff/profile/edit.js +5 -3
  23. package/extend/waibuMpa/route/your-stuff/reset-api-key.js +3 -1
  24. package/index.js +33 -0
  25. package/package.json +1 -1
  26. package/wiki/CHANGES.md +10 -0
@@ -1,16 +1,16 @@
1
- async function afterCreateRecord (body, options = {}, rec) {
2
- if (!options.req) return
3
- if (!this.app.waibu) return
4
- const { sendMail } = this.app.waibu
1
+ async function afterCreateRecord (body, rec, options = {}) {
5
2
  const { data } = rec
6
3
  const { req } = options
4
+ const { get } = this.app.lib._
5
+ const t = get(req, 't', this.t)
7
6
  const to = `${data.firstName} ${data.lastName} <${data.email}>`
8
7
  let bcc
9
- if (req && req.site) bcc = req.site.email
10
- const subject = options.req.t('contactForm')
11
- await sendMail(
8
+ if (req.site) bcc = req.site.email
9
+ const subject = t('contactForm')
10
+ const payload = { to, bcc, subject, data }
11
+ await this.sendMail(
12
12
  'sumba.template:/_mail/help-contact-form.html',
13
- { to, bcc, subject, data, options, source: this.ns }
13
+ { payload, options, source: this.ns }
14
14
  )
15
15
  }
16
16
 
@@ -1,13 +1,14 @@
1
1
  async function afterCreateRecord (body, rec, options = {}) {
2
- if (!options.req) return
3
- if (!this.app.waibu) return
4
- const { sendMail } = this.app.waibu
5
2
  const { data } = rec
3
+ const { req } = options
4
+ const { get } = this.app.lib._
5
+ const t = get(req, 't', this.t)
6
6
  const to = `${data.firstName} ${data.lastName} <${data.email}>`
7
- const subject = options.req.t('newUserSignup')
8
- await sendMail(
7
+ const subject = t('newUserSignup')
8
+ const payload = { to, subject, data }
9
+ await this.sendMail(
9
10
  `sumba.template:/_mail/user-signup-success${data.status === 'ACTIVE' ? '-active' : ''}.html`,
10
- { to, subject, data, options, source: this.ns }
11
+ { payload, options, source: this.ns }
11
12
  )
12
13
  }
13
14
 
@@ -1,28 +1,30 @@
1
1
  async function afterUpdateRecord (id, body, rec, options = {}) {
2
- if (!options.req) return
3
- if (!this.app.waibu) return
4
2
  const { data, oldData } = rec
5
- const { sendMail } = this.app.waibu
3
+ const { req } = options
4
+ const { get } = this.app.lib._
5
+ const t = get(req, 't', this.t)
6
6
  const to = `${data.firstName} ${data.lastName} <${data.email}>`
7
+ const source = this.ns
7
8
  let subject
9
+ const payload = { to, subject, data }
8
10
 
9
11
  if (oldData.status === 'UNVERIFIED' && data.status === 'ACTIVE') {
10
- subject = options.req.t('userActivation')
11
- await sendMail(
12
+ payload.subject = t('userActivation')
13
+ await this.sendMail(
12
14
  'sumba.template:/_mail/user-activation-success.html',
13
- { to, subject, data, options, source: this.ns }
15
+ { payload, options, source }
14
16
  )
15
17
  } else if (oldData.token !== data.token) {
16
- subject = options.req.t('resetApiKey')
17
- await sendMail(
18
+ payload.subject = t('resetApiKey')
19
+ await this.sendMail(
18
20
  'sumba.template:/_mail/mystuff-reset-api-key.html',
19
- { to, subject, data, options, source: this.ns }
21
+ { payload, options, source }
20
22
  )
21
23
  } else if (body.password) {
22
- subject = options.req.t('changePassword')
23
- await sendMail(
24
+ payload.subject = t('changePassword')
25
+ await this.sendMail(
24
26
  'sumba.template:/_mail/mystuff-change-password.html',
25
- { to, subject, data, options, source: this.ns }
27
+ { payload, options, source }
26
28
  )
27
29
  }
28
30
  }
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <head lang="<%= _meta.lang %>">
3
+ <meta charset="UTF-8">
4
+ <title><%= page.fullTitle %></title>
5
+ <style type="text/css">
6
+ </style>
7
+ </head>
8
+ <body>
9
+ <% if (arguments[0].firstName && arguments[0].lastName) { %>
10
+ <p>Dear <%= firstName %> <%= lastName %>,</p>
11
+ <% } %>
12
+ <!-- body -->
13
+ <p>Sincerely,</p>
14
+
15
+ <p><%= _meta.site.title %></p>
16
+ </body>
17
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= _meta.lang %>">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title><%= page.fullTitle %></title>
6
+ <style type="text/css">
7
+ </style>
8
+ </head>
9
+ <body>
10
+ <% if (arguments[0].firstName && arguments[0].lastName) { %>
11
+ <p>Yth <%= firstName %> <%= lastName %>,</p>
12
+ <% } %>
13
+ <!-- body -->
14
+ <p>Hormat kami,</p>
15
+
16
+ <p><%= _meta.site.title %></p>
17
+ </body>
18
+ </html>
@@ -4,7 +4,7 @@
4
4
  <table>
5
5
  <tr>
6
6
  <td>Category:</td>
7
- <td><%= cat %></td>
7
+ <td><%= category %></td>
8
8
  </tr>
9
9
  <tr>
10
10
  <td>Subject:</td>
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -1,3 +1,3 @@
1
1
  ---
2
- layout: waibu.layout:/email.html
2
+ layout: sumba.layout:/email.html
3
3
  ---
@@ -3,7 +3,7 @@
3
3
  "firstName,,50,true,true",
4
4
  "lastName,,50,true,true",
5
5
  "email,,50,true,true",
6
- "cat,,50,true",
6
+ "category,,50,true",
7
7
  "subject,,255,,true",
8
8
  "message,text"
9
9
  ],
@@ -10,7 +10,7 @@ const contactForm = {
10
10
  let error
11
11
  if (req.method === 'POST') {
12
12
  try {
13
- const { data } = await createRecord({ model: 'SumbaContactForm', req, reply, options: { noFlash: true } })
13
+ const { data } = await createRecord({ model: 'SumbaContactForm', req, reply, transaction: true, options: { noFlash: true } })
14
14
  req.flash('notify', req.t('contactFormSubmitted'))
15
15
  return await reply.view('sumba.template:/help/contact-form/success.html', { form: req.body, data })
16
16
  } catch (err) {
@@ -10,7 +10,9 @@ const userActivation = {
10
10
  const query = { status: 'UNVERIFIED', token: req.body.key }
11
11
  const result = await model.findRecord({ query, limit: 1 })
12
12
  if (result.length === 0) throw this.error('validationError', { details: [{ field: 'key', error: 'invalidActivationKey' }] })
13
- await model.updateRecord(result[0].id, { status: 'ACTIVE' }, { req, noValidation: true, noFlash: true })
13
+ await model.transaction(async (trx) => {
14
+ await model.updateRecord(result[0].id, { status: 'ACTIVE' }, { req, noValidation: true, noFlash: true, trx })
15
+ })
14
16
  req.flash('notify', req.t('userActivated'))
15
17
  return reply.redirectTo(this.config.redirect.signin, req)
16
18
  } catch (err) {
@@ -17,7 +17,6 @@ async function getUser (req, reply) {
17
17
  const forgotPasswordLink = {
18
18
  method: ['GET', 'POST'],
19
19
  handler: async function (req, reply) {
20
- const { sendMail } = this.app.waibu
21
20
  const { defaultsDeep } = this.app.lib.aneka
22
21
  const { importPkg } = this.app.bajo
23
22
  const { isString } = this.app.lib._
@@ -41,14 +40,17 @@ const forgotPasswordLink = {
41
40
  } catch (err) {
42
41
  throw this.error('validationError', { details: err.details, values: err.values, ns: this.ns, statusCode: 422, code: 'DB_VALIDATION' })
43
42
  }
44
- await model.updateRecord(user.id, { password: req.body.newPassword }, { noFlash: true })
45
- const to = `${user.firstName} ${user.lastName} <${user.email}>`
46
- const subject = req.t('forgotPasswordChanged')
47
- const options = { req, reply, tpl: '' }
48
- await sendMail(
49
- 'sumba.template:/_mail/user-forgot-password-changed.html',
50
- { to, subject, data: user, options, source: this.ns }
51
- )
43
+ await model.transaction(async trx => {
44
+ await model.updateRecord(user.id, { password: req.body.newPassword }, { noFlash: true, trx })
45
+ const to = `${user.firstName} ${user.lastName} <${user.email}>`
46
+ const subject = req.t('forgotPasswordChanged')
47
+ const options = { req, reply, tpl: '' }
48
+ const payload = { to, subject, data: user }
49
+ await this.sendMail(
50
+ 'sumba.template:/_mail/user-forgot-password-changed.html',
51
+ { payload, options, source: this.ns }
52
+ )
53
+ })
52
54
  req.flash('notify', req.t('passwordChangedReSignin'))
53
55
  return reply.redirectTo(this.config.redirect.signin)
54
56
  } catch (err) {
@@ -2,7 +2,6 @@ const profile = {
2
2
  method: ['GET', 'POST'],
3
3
  handler: async function (req, reply) {
4
4
  if (!this.app.masohiMail) return await reply.view('sumba.template:/user/forgot-password.html')
5
- const { sendMail } = this.app.waibu
6
5
  const { defaultsDeep } = this.app.lib.aneka
7
6
  const { dayjs } = this.app.lib
8
7
  const model = this.app.dobo.getModel('SumbaUser')
@@ -20,9 +19,10 @@ const profile = {
20
19
  const exp = req.site.setting.sumba.forgotPasswordExpDur
21
20
  data.fpToken = Buffer.from(`${data.token}:${dayjs().add(exp, 'ms').unix()}`).toString('base64')
22
21
  data._meta = { hostHeader: req.headers.host }
23
- await sendMail(
22
+ const payload = { to, subject, data }
23
+ await this.sendMail(
24
24
  'sumba.template:/_mail/user-forgot-password-link.html',
25
- { to, subject, data, options, source: this.ns }
25
+ { payload, options, source: this.ns }
26
26
  )
27
27
  req.flash('notify', req.t('emailSent'))
28
28
  return reply.redirectTo(this.config.redirect.signin)
@@ -24,7 +24,7 @@ const signup = {
24
24
  const validation = { ns: ['sumba', 'dobo'], fields, extFields }
25
25
  req.body.token = generateId()
26
26
  req.body.provider = 'local'
27
- const { data } = await createRecord({ model: 'SumbaUser', req, reply, options: { validation, noFlash: true, forceNoHidden: true } })
27
+ const { data } = await createRecord({ model: 'SumbaUser', req, reply, transaction: true, options: { validation, noFlash: true, forceNoHidden: true } })
28
28
  req.flash('notify', req.t('userCreated'))
29
29
  return await reply.view('sumba.template:/user/signup/success.html', { form: req.body, data })
30
30
  } catch (err) {
@@ -26,7 +26,9 @@ const profile = {
26
26
  const rec = await model.getRecord(req.user.id, { forceNoHidden: true })
27
27
  const verified = await bcrypt.compare(req.body.currentPassword, rec.password)
28
28
  if (!verified) throw this.error('invalidCurrentPassword', { details: [{ field: 'currentPassword', error: 'invalidPassword' }], statusCode: 400 })
29
- await model.updateRecord(req.user.id, { password: req.body.newPassword }, { req, reply, noFlash: true })
29
+ await model.transaction(async (trx) => {
30
+ await model.updateRecord(req.user.id, { password: req.body.newPassword }, { req, reply, noFlash: true, trx })
31
+ })
30
32
  // signout and redirect to signin
31
33
  req.session.userId = null
32
34
  req.flash('notify', req.t('passwordChangedReSignin'))
@@ -1,3 +1,5 @@
1
+ const fields = ['email', 'firstName', 'lastName', 'address1', 'address2', 'city', 'zipCode', 'provinceState', 'country', 'phone', 'website']
2
+
1
3
  const profile = {
2
4
  method: ['GET', 'POST'],
3
5
  handler: async function (req, reply) {
@@ -7,13 +9,13 @@ const profile = {
7
9
  const { omit, pick } = this.app.lib._
8
10
  const { hash } = this.app.bajoExtra
9
11
  const resp = await getRecord({ model: 'SumbaUser', req, id: req.user.id, options: { forceNoHidden: true, noHook: true, noCache: true } })
10
- let form = defaultsDeep(req.body, omit(resp.data, ['password']))
12
+ let form = defaultsDeep(req.body, omit(resp.data, ['password', 'salt']))
11
13
  form.token = await hash(form.token)
12
14
  let error
13
15
  if (req.method === 'POST') {
14
16
  try {
15
- const body = pick(form, ['firstName', 'lastName', 'address1', 'address2', 'city', 'zipCode', 'provinceState', 'country', 'phone', 'website'])
16
- const options = { noFlash: true, hidden: [], setField: 'profile', setFile: 'main.png' }
17
+ const body = pick(form, fields)
18
+ const options = { noFlash: true, hidden: [], setField: 'profile', setFile: 'main.png', partial: true, fields }
17
19
  const resp = await updateRecord({ req, reply, model: 'SumbaUser', id: req.user.id, body, options })
18
20
  form = resp.data
19
21
  req.flash('notify', req.t('profileUpdated'))
@@ -24,7 +24,9 @@ const resetApiKey = {
24
24
  const rec = await model.getRecord(req.user.id, { forceNoHidden: true })
25
25
  const verified = await bcrypt.compare(form.password, rec.password)
26
26
  if (!verified) throw this.error('validationError', { details: [{ field: 'password', error: 'invalidPassword' }], statusCode: 400 })
27
- await model.updateRecord(req.user.id, { salt: generateId() }, { req, reply, noFlash: true, forceNoHidden: true })
27
+ await model.transaction(async (trx) => {
28
+ await model.updateRecord(req.user.id, { salt: generateId() }, { req, reply, noFlash: true, forceNoHidden: true, trx })
29
+ })
28
30
  await delay(2000) // ensure req.user cache is expired
29
31
  req.flash('notify', req.t('resetApiKeySuccessfull'))
30
32
  return reply.redirectTo('sumba:/your-stuff/profile')
package/index.js CHANGED
@@ -541,6 +541,39 @@ async function factory (pkgName) {
541
541
  const items = await model.findAllRecord()
542
542
  return items.map(item => ({ value: item.id, text: item.name }))
543
543
  }
544
+
545
+ /**
546
+ * Method to send mail through Masohi Messaging System. It is a thin wrapper
547
+ * for {@link https://github.com/ardhi/masohi-mail|masohi-mail} send method.
548
+ *
549
+ * If both masohiMail and waibu are not loaded, nothing is delivered.
550
+ *
551
+ * @method
552
+ * @async
553
+ * @param {(string|Array)} tpl - Mail's template to use. If a string is given, the same template will be used for html & plaintext versions. Otherwise, the first template will be used for html mail, and the second one is for it's plaintext version
554
+ * @param {Object} [params={}] - {@link https://github.com/ardhi/masohi-mail|masohi-mail}'s params object.
555
+ * @returns
556
+ */
557
+ sendMail = async (tpl, { payload = {}, conn, source, options = {} } = {}) => {
558
+ if (!this.app.masohiMail || !this.app.waibu) return
559
+ conn = conn ?? 'masohiMail:default'
560
+ const { importModule } = this.app.bajo
561
+ const { get, isString } = this.app.lib._
562
+ const { generateId } = this.app.lib.aneka
563
+ const { render } = this.app.bajoTemplate
564
+ const buildLocals = await importModule('waibu:/lib/build-locals.js')
565
+
566
+ if (isString(tpl)) tpl = [tpl]
567
+ const locals = await buildLocals.call(this, { params: payload.data, opts: options })
568
+ payload.from = payload.from ?? get(options, 'req.site.email', payload.from)
569
+ const opts = {
570
+ lang: get(options, 'req.lang'),
571
+ groupId: get(options, 'req.id', generateId())
572
+ }
573
+ payload.html = await render(tpl[0], locals, opts)
574
+ if (tpl[1]) payload.text = await render(tpl[1], locals, opts)
575
+ await this.app.masohiMail.send({ payload, source: source ?? this.ns, conn })
576
+ }
544
577
  }
545
578
 
546
579
  return Sumba
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Biz Suite for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-03-05
4
+
5
+ - [2.6.0] Update multiple endpoints to support dobo's transaction
6
+ - [2.6.0] Bug fix on ```profile.edit``` route
7
+
8
+ ## 2026-03-02
9
+
10
+ - [2.5.0] Add ```sendMail()``` to send mail using ```masohiMail```
11
+ - [2.5.0] Update all mail templates and hooks to match with the new specs
12
+
3
13
  ## 2026-02-21
4
14
 
5
15
  - [2.4.1] Bug fix on intl functions. Now moved to ```preParsing``` instead of ```onRequest```