sumba 2.20.0 → 2.22.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,6 @@
1
1
  export async function clearCache (id, result) {
2
2
  if (!this.app.bajoCache) return
3
- const { clear } = this.app.bajoCache
3
+ const { clear } = this.app.bajoCache ?? {}
4
4
  const { get } = this.app.lib._
5
5
  await clear({ key: 'dobo|SumbaSite|getSite|default' })
6
6
  await clear({ key: `dobo|SumbaSite|getSite|multiSite|${id}` })
@@ -3,7 +3,6 @@ async function afterRecordValidation (body, options) {
3
3
  const { has } = this.app.lib._
4
4
 
5
5
  if (has(body, 'password') && !isBcrypt(body.password)) body.password = await hash(body.password, 'bcrypt')
6
- // if (has(body, 'token') && !isMd5(body.token)) body.token = await hash(body.token)
7
6
  }
8
7
 
9
8
  export default afterRecordValidation
@@ -1,7 +1,7 @@
1
1
  export async function clearCache (id, result) {
2
2
  if (!this.app.bajoCache) return
3
3
  const { hash } = this.app.bajoExtra
4
- const { clear } = this.app.bajoCache
4
+ const { clear } = this.app.bajoCache ?? {}
5
5
  const { get, isEmpty } = this.app.lib._
6
6
  let token = get(result, 'data.token', get(result, 'oldData.token', ''))
7
7
  if (!isEmpty(token)) token = await hash(token)
@@ -158,7 +158,8 @@
158
158
  "user": "User",
159
159
  "team": "Team",
160
160
  "ns": "Module's Namespace",
161
- "value": "Value"
161
+ "value": "Value",
162
+ "notes": "Notes"
162
163
  },
163
164
  "validation": {
164
165
  "password": {
@@ -159,7 +159,8 @@
159
159
  "user": "Pengguna",
160
160
  "team": "Tim",
161
161
  "ns": "Ruang Nama Modul",
162
- "value": "Value"
162
+ "value": "Value",
163
+ "notes": "Catatan"
163
164
  },
164
165
  "validation": {
165
166
  "password": {
@@ -1,12 +1,10 @@
1
- <c:modal id="api-key-modal" t:title="apiKey">
2
- <c:modal-body>
3
- <pre><code id="api-key"><%= form.token %></code></pre>
4
- <c:div flex="justify-content:between align-items:center" margin="top-3">
5
- <c:a icon="arrowsRepeat" href="sumba:/your-stuff/reset-api-key" t:content="reset" />
6
- <div>
7
- <c:btn color="primary" x-data @click="await wbs.copyToClipboard('#api-key', true)" t:content="copyClipboard" />
8
- <c:btn margin="start-1" color="secondary" dismiss t:content="close" />
9
- </div>
10
- </c:div>
11
- </c:modal-body>
1
+ <c:modal id="api-key-modal" t:title="apiKey" size="lg">
2
+ <pre><code id="api-key"><%= form.apiKey %></code></pre>
3
+ <c:div flex="justify-content:between align-items:center" margin="top-3">
4
+ <c:a icon="arrowsRepeat" href="sumba:/your-stuff/reset-api-key" t:content="reset" />
5
+ <div>
6
+ <c:btn color="primary" x-data @click="await wbs.copyToClipboard('#api-key', true)" t:content="copyClipboard" />
7
+ <c:btn margin="start-1" color="secondary" dismiss t:content="close" />
8
+ </div>
9
+ </c:div>
12
10
  </c:modal>
@@ -1,14 +1,16 @@
1
1
  async function country (opts = {}) {
2
2
  opts.field = opts.field ?? 'country'
3
+ opts.enforceRule = true
3
4
  return {
4
5
  properties: [{
5
6
  name: opts.field,
6
7
  type: 'string',
7
8
  maxLength: 2,
8
9
  index: opts.index ?? true,
10
+ required: opts.required,
9
11
  values: 'sumba:getCountriesValues',
10
- rules: ['uppercase', { rule: 'length', params: 2 }],
11
- rulesMsg: { 'any.only': 'validCountryCodeRequired' }
12
+ rules: opts.enforceRule ? ['uppercase', { rule: 'length', params: 2 }] : [],
13
+ rulesMsg: opts.enforceRule ? { 'any.only': 'validCountryCodeRequired' } : undefined
12
14
  }],
13
15
  rules: [{ rule: 'trim', fields: [opts.field] }]
14
16
  }
@@ -0,0 +1,11 @@
1
+ async function notes (opts = {}) {
2
+ opts.field = opts.field ?? 'notes'
3
+ return {
4
+ properties: [{
5
+ name: opts.field,
6
+ type: 'text'
7
+ }]
8
+ }
9
+ }
10
+
11
+ export default notes
@@ -0,0 +1,83 @@
1
+ async function user () {
2
+ return {
3
+ buildLevel: 2,
4
+ properties: [{
5
+ name: 'username',
6
+ type: 'string',
7
+ minLength: 5,
8
+ maxLength: 50,
9
+ rules: ['alphanum']
10
+ }, {
11
+ name: 'password',
12
+ type: 'string',
13
+ minLength: 8,
14
+ maxLength: 100
15
+ }, {
16
+ name: 'token',
17
+ type: 'string',
18
+ maxLength: 100,
19
+ index: true
20
+ }, {
21
+ name: 'salt',
22
+ type: 'string',
23
+ maxLength: 100,
24
+ required: true
25
+ }, {
26
+ name: 'apiKey',
27
+ type: 'string',
28
+ maxLength: 100,
29
+ virtual: true,
30
+ getValue: async function (val, rec) {
31
+ return await this.plugin.hash(rec.salt)
32
+ }
33
+ }, {
34
+ name: 'provider',
35
+ type: 'string',
36
+ maxLength: 50,
37
+ index: true,
38
+ default: 'local'
39
+ }, {
40
+ name: 'email',
41
+ type: 'string',
42
+ maxLength: 100,
43
+ required: true,
44
+ rules: ['email']
45
+ }, {
46
+ name: 'firstName',
47
+ type: 'string',
48
+ maxLength: 50,
49
+ required: true,
50
+ index: true
51
+ }, {
52
+ name: 'lastName',
53
+ type: 'string',
54
+ maxLength: 50,
55
+ required: true,
56
+ index: true
57
+ }],
58
+ rules: [{ rule: 'trim', fields: ['username', 'firstName', 'lastName'] }],
59
+ indexes: [{
60
+ fields: ['username', 'siteId'],
61
+ type: 'unique'
62
+ }, {
63
+ fields: ['email', 'siteId'],
64
+ type: 'unique'
65
+ }],
66
+ hidden: ['password', 'token'],
67
+ features: [
68
+ 'sumba:address',
69
+ 'sumba:social',
70
+ {
71
+ name: 'sumba:status',
72
+ default: 'UNVERIFIED',
73
+ values: ['UNVERIFIED', 'ACTIVE', 'INACTIVE']
74
+ },
75
+ 'sumba:siteId',
76
+ 'dobo:createdAt',
77
+ 'dobo:updatedAt',
78
+ 'dobo:immutable'
79
+ ]
80
+ }
81
+ }
82
+
83
+ export default user
@@ -8,11 +8,6 @@ async function teamUser () {
8
8
  calcFields: [
9
9
  { name: 'team', type: 'string' }
10
10
  ],
11
- formatValue: {
12
- team: (val, rec) => {
13
- return rec._ref.team.name
14
- }
15
- },
16
11
  widget: {
17
12
  teamId: {
18
13
  component: 'form-select-ext',
@@ -5,14 +5,6 @@ async function teamUser ({ req } = {}) {
5
5
  { name: 'meta', fields: ['id', 'userId', 'createdAt', 'updatedAt'] },
6
6
  { name: 'general', fields: ['ns', 'key', 'value'] }
7
7
  ],
8
- calcFields: [
9
- { name: 'user', type: 'string' }
10
- ],
11
- formatValue: {
12
- user: (val, rec) => {
13
- return rec._ref.user.name
14
- }
15
- },
16
8
  widget: {
17
9
  userId: {
18
10
  component: 'form-select-ext',
@@ -3,16 +3,10 @@ async function user ({ req } = {}) {
3
3
  common: {
4
4
  layout: [
5
5
  { name: 'meta', fields: ['id:3', 'createdAt:3', 'updatedAt:3', 'status:3'] },
6
- { name: 'account', fields: ['username:3', 'email:3', 'provider:3', 'password:3', 'firstName:3', 'lastName:3', 'token:6'] },
6
+ { name: 'account', fields: ['username:3', 'email:3', 'provider:3', 'password:3', 'firstName:3', 'lastName:3', 'apiKey:6'] },
7
7
  { name: 'address', fields: ['address1:12', 'address2:12', 'city:6-md 8-sm', 'zipCode:2-md 4-sm', 'provinceState:4-md', 'country:6-md', 'phone:6-md', 'website:12'] },
8
8
  { name: 'socialMedia', fields: ['socX:3-md 6-sm', 'socInstagram:3-md 6-sm', 'socFacebook:3-md 6-sm', 'socLinkedIn:3-md 6-sm'] }
9
9
  ],
10
- formatValue: {
11
- token: async function (val, rec) {
12
- const { hash } = this.app.bajoExtra
13
- return await hash(rec.salt)
14
- }
15
- },
16
10
  widget: {
17
11
  token: {
18
12
  component: 'form-plaintext'
@@ -25,7 +19,7 @@ async function user ({ req } = {}) {
25
19
  sort: 'username:1',
26
20
  limit: 10
27
21
  },
28
- fields: ['createdAt', 'status', 'username', 'provider', 'email', 'firstName', 'lastName', 'city', 'zipCode', 'provinceState', 'country', 'phone'],
22
+ fields: ['createdAt', 'status', 'username', 'provider', 'email', 'firstName', 'lastName', 'city', 'zipCode', 'provinceState', 'country', 'phone', 'apiKey'],
29
23
  stat: {
30
24
  aggregate: [
31
25
  { fields: ['status'], group: 'status', aggregate: ['count'] },
@@ -13,8 +13,7 @@ const manageSite = {
13
13
  req.params.base = ''
14
14
  const options = {
15
15
  modelOpts: {
16
- formatValue: true,
17
- retainOriginalValue: true,
16
+ fmt: true,
18
17
  noCache: true
19
18
  },
20
19
  schema: {
@@ -8,16 +8,14 @@ const profile = {
8
8
  const { updateRecord, getRecord } = this.app.waibuDb
9
9
  const { getSchemaExt } = this.app.waibuDb
10
10
  const { omit, pick } = this.app.lib._
11
- const { hash } = this.app.bajoExtra
12
11
 
13
- const options = { forceNoHidden: ['token'], noHook: true, noCache: true, formatValue: true, retainOriginalValue: true }
12
+ const options = { forceNoHidden: ['token'], noHook: true, noCache: true, fmt: true }
14
13
  const mdl = this.app.dobo.getModel(model)
15
14
 
16
15
  const { schema } = await getSchemaExt(model, 'edit', { ...options, args: [{ req, model: mdl }] })
17
16
 
18
17
  const resp = await getRecord({ model, req, id: req.user.id, options })
19
18
  let form = defaultsDeep(req.body, omit(resp.data, ['password', 'salt']))
20
- form.token = await hash(form.token)
21
19
  let error
22
20
  if (req.method === 'POST') {
23
21
  try {
@@ -1,12 +1,10 @@
1
1
  const profile = {
2
2
  method: ['GET'],
3
3
  handler: async function (req, reply) {
4
- const { hash } = this.app.bajoExtra
5
4
  const { getRecord } = this.app.waibuDb
6
- const options = { forceNoHidden: ['token'], noHook: true, noCache: true, attachment: true, mimeType: true, formatValue: true, retainOriginalValue: true }
5
+ const options = { forceNoHidden: ['token'], noHook: true, noCache: true, attachment: true, mimeType: true, fmt: true }
7
6
  const resp = await getRecord({ model: 'SumbaUser', req, id: req.user.id, options })
8
7
  const form = resp.data
9
- form.token = await hash(form.salt)
10
8
  return await reply.view('sumba.template:/your-stuff/profile/view.html', { form })
11
9
  }
12
10
  }
@@ -4,11 +4,10 @@ const resetApiKey = {
4
4
  const { defaultsDeep } = this.app.lib.aneka
5
5
  const { importPkg } = this.app.bajo
6
6
  const { generateId } = this.app.lib.aneka
7
- const { hash } = this.app.bajoExtra
8
7
  const delay = await importPkg('bajo:delay')
9
8
  const bcrypt = await importPkg('bajoExtra:bcrypt')
10
9
  const Joi = await importPkg('dobo:joi')
11
- const form = defaultsDeep(req.body, { apiKey: await hash(req.user.salt) })
10
+ const form = defaultsDeep(req.body, { apiKey: req.user.apiKey })
12
11
  const model = this.app.dobo.getModel('SumbaUser')
13
12
  let error
14
13
  if (req.method === 'POST') {
@@ -21,7 +20,7 @@ const resetApiKey = {
21
20
  } catch (err) {
22
21
  throw this.error('validationError', { details: err.details, values: err.values, ns: this.ns, statusCode: 422, code: 'DB_VALIDATION' })
23
22
  }
24
- const rec = await model.getRecord(req.user.id, { forceNoHidden: true })
23
+ const rec = await model.getRecord(req.user.id, { forceNoHidden: true, noMagic: true })
25
24
  const verified = await bcrypt.compare(form.password, rec.password)
26
25
  if (!verified) throw this.error('validationError', { details: [{ field: 'password', error: 'invalidPassword' }], statusCode: 400 })
27
26
  await model.transaction(async (trx) => {
@@ -13,15 +13,13 @@ async function create () {
13
13
  }
14
14
 
15
15
  const handler = async function (req, reply) {
16
- const { hash } = this.app.bajoExtra
17
-
18
16
  if (!['api-key', 'jwt', 'apiKey'].includes(req.params.type)) throw this.error('invalidTokenType')
19
17
  const rec = await this.getUserByUsernamePassword(req.body.username, req.body.password, req)
20
18
  if (req.params.type === 'jwt') {
21
19
  const jwt = await this.createJwtFromUserRecord(rec)
22
20
  return { data: jwt }
23
21
  }
24
- return { data: { token: await hash(rec.salt) } }
22
+ return { data: { token: rec.apiKey } }
25
23
  }
26
24
  return { schema, handler }
27
25
  }
@@ -12,10 +12,8 @@ async function get () {
12
12
  }
13
13
 
14
14
  const handler = async function (req, reply, options) {
15
- const { hash } = this.app.bajoExtra
16
-
17
15
  const profile = await this.app.dobo.getModel('SumbaUser').getRecord(req.user.id)
18
- return { data: { token: await hash(profile.password) } }
16
+ return { data: { token: profile.apiKey } }
19
17
  }
20
18
  return { schema, handler }
21
19
  }
@@ -1,11 +1,10 @@
1
1
  import { response } from './update.js'
2
2
 
3
3
  async function get () {
4
- const { hash } = this.app.bajoExtra
5
4
  const schema = { response: await response.call(this) }
6
5
  const handler = async function get (req, reply) {
7
6
  const rec = await this.app.dobo.getModel('SumbaUser').getRecord(req.user.id, { forceNoHidden: true })
8
- return { data: { token: await hash(rec.salt) } }
7
+ return { data: { token: rec.apiKey } }
9
8
  }
10
9
  return { schema, handler }
11
10
  }
@@ -22,7 +22,6 @@ export const body = {
22
22
  async function update () {
23
23
  const { importPkg } = this.app.bajo
24
24
  const { generateId } = this.app.lib.aneka
25
- const { hash } = this.app.bajoExtra
26
25
  const bcrypt = await importPkg('bajoExtra:bcrypt')
27
26
  const model = this.app.dobo.getModel('SumbaUser')
28
27
 
@@ -34,7 +33,7 @@ async function update () {
34
33
  if (!verified) throw this.error('invalidPassword', { details: [{ field: 'password', error: 'invalidPassword' }], statusCode: 400 })
35
34
  const input = { salt: generateId() }
36
35
  const resp = await model.updateRecord(req.user.id, input, { forceNoHidden: true })
37
- return { data: { token: await hash(resp.salt) } }
36
+ return { data: { token: resp.apiKey } }
38
37
  }
39
38
 
40
39
  return { schema, handler }
package/index.js CHANGED
@@ -91,7 +91,8 @@ async function factory (pkgName) {
91
91
  apiKey: {
92
92
  type: 'Bearer',
93
93
  qsKey: 'apiKey',
94
- headerKey: 'X-Auth-ApiKey'
94
+ headerKey: 'X-Auth-ApiKey',
95
+ algo: 'sha256' // changing this require each and every user to reset their apiKey
95
96
  },
96
97
  basic: {
97
98
  },
@@ -251,7 +252,6 @@ async function factory (pkgName) {
251
252
  createJwtFromUserRecord = async (rec) => {
252
253
  const { importPkg } = this.app.bajo
253
254
  const { dayjs } = this.app.lib
254
- const { hash } = this.app.bajoExtra
255
255
  const { get, pick } = this.app.lib._
256
256
 
257
257
  const fastJwt = await importPkg('bajoExtra:fast-jwt')
@@ -260,7 +260,7 @@ async function factory (pkgName) {
260
260
  const opts = pick(this.config.auth.common.jwt, ['expiresInDur'])
261
261
  opts.key = get(this.config, 'auth.common.jwt.secret')
262
262
  const sign = createSigner(opts)
263
- const apiKey = await hash(rec.password)
263
+ const apiKey = await this.hash(rec.token)
264
264
  const payload = { uid: rec.id, apiKey }
265
265
  const token = await sign(payload)
266
266
  const expiresAt = dayjs().add(opts.expiresInDur).toDate()
@@ -281,12 +281,12 @@ async function factory (pkgName) {
281
281
  }
282
282
 
283
283
  verifyApiKey = async (req, reply, source, payload) => {
284
- const { merge } = this.app.lib._
285
- const { isMd5, hash } = this.app.bajoExtra
284
+ const { merge, camelCase } = this.app.lib._
285
+ const checker = this.app.bajoExtra[camelCase(`is ${this.config.auth.common.apiKey.algo}`)]
286
286
 
287
287
  let token = await this._getToken('apiKey', req, source)
288
- if (!isMd5(token)) return false
289
- token = await hash(token)
288
+ if (!checker(token)) return false
289
+ token = await this.hash(token)
290
290
  const user = await this.getUserByToken(token, req)
291
291
  if (!user) throw this.error('invalidKey', merge({ statusCode: 401 }, payload))
292
292
  if (user.status !== 'ACTIVE') throw this.error('userInactive', merge({ details: [{ field: 'status', error: 'inactive' }], statusCode: 401 }, payload))
@@ -463,10 +463,9 @@ async function factory (pkgName) {
463
463
  }
464
464
 
465
465
  getApiKeyFromUserId = async id => {
466
- const { hash } = this.app.bajoExtra
467
466
  const options = { forceNoHidden: true, noHook: true, noCache: true, attachment: true, mimeType: true }
468
467
  const resp = await getModel('SumbaUser').getRecord(id, options)
469
- return await hash(resp.salt)
468
+ return await this.hash(resp.salt)
470
469
  }
471
470
 
472
471
  getCountriesValues = async () => {
@@ -509,11 +508,10 @@ async function factory (pkgName) {
509
508
  await this.app.masohiMail.send({ payload, source: source ?? this.ns, conn })
510
509
  }
511
510
 
512
- resetToken = async (salt) => {
511
+ resetToken = async (text) => {
513
512
  const { generateId } = this.app.lib.aneka
514
- const { hash } = this.app.bajoExtra
515
- salt = salt ?? generateId()
516
- const token = await hash(await hash(salt))
513
+ const salt = text ?? generateId()
514
+ const token = await this.hash(await this.hash(salt))
517
515
  return { salt, token }
518
516
  }
519
517
 
@@ -557,6 +555,11 @@ async function factory (pkgName) {
557
555
  return item
558
556
  }
559
557
 
558
+ hash = async (item) => {
559
+ const { hash } = this.app.bajoExtra
560
+ return await hash(item, this.config.auth.common.apiKey.algo)
561
+ }
562
+
560
563
  createNewSite = createNewSite
561
564
  removeSite = removeSite
562
565
  getSite = getSite
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.20.0",
3
+ "version": "2.22.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,13 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-04-25
4
+
5
+ - [2.21.0] Change options to format value using the new key set by dobo
6
+ - [2.21.0] Remove ```options.retainOriginalValue``` since it is not needed anymore
7
+ - [2.21.0] Remove ```property.formatValue``` from all properties
8
+ - [2.21.0] Remove ```schema.formatValue``` from all schemas
9
+ - [2.21.0] Remove ```schema.calcFields``` from all schemas
10
+
3
11
  ## 2026-04-23
4
12
 
5
13
  - [2.20.0] Add ```parseRouteGuard()```
@@ -1,70 +0,0 @@
1
- {
2
- "buildLevel": 2,
3
- "properties": [{
4
- "name": "username",
5
- "type": "string",
6
- "minLength": 5,
7
- "maxLength": 50,
8
- "rules": ["alphanum"]
9
- }, {
10
- "name": "password",
11
- "type": "string",
12
- "minLength": 8,
13
- "maxLength": 100
14
- }, {
15
- "name": "token",
16
- "type": "string",
17
- "maxLength": 50,
18
- "index": true
19
- }, {
20
- "name": "salt",
21
- "type": "string",
22
- "maxLength": 50,
23
- "required": true
24
- }, {
25
- "name": "provider",
26
- "type": "string",
27
- "maxLength": 50,
28
- "index": true,
29
- "default": "local"
30
- }, {
31
- "name": "email",
32
- "type": "string",
33
- "maxLength": 100,
34
- "required": true,
35
- "rules": ["email"]
36
- }, {
37
- "name": "firstName",
38
- "type": "string",
39
- "maxLength": 50,
40
- "required": true,
41
- "index": true
42
- }, {
43
- "name": "lastName",
44
- "type": "string",
45
- "maxLength": 50,
46
- "required": true,
47
- "index": true
48
- }],
49
- "rules": [{ "rule": "trim", "fields": ["username", "firstName", "lastName"] }],
50
- "indexes": [{
51
- "fields": ["username", "siteId"],
52
- "type": "unique"
53
- }, {
54
- "fields": ["email", "siteId"],
55
- "type": "unique"
56
- }],
57
- "hidden": ["password", "token"],
58
- "features": [
59
- "sumba:address",
60
- "sumba:social",
61
- {
62
- "name": "sumba:status",
63
- "default": "UNVERIFIED"
64
- },
65
- "sumba:siteId",
66
- "dobo:createdAt",
67
- "dobo:updatedAt",
68
- "dobo:immutable"
69
- ]
70
- }