sumba 2.9.0 → 2.11.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.
@@ -4,11 +4,22 @@ export async function rebuildFilter (modelName, filter, req) {
4
4
  const { isEmpty, isPlainObject, map, find, get } = this.app.lib._
5
5
  filter.query = filter.query ?? {}
6
6
 
7
- const queryByModel = (query) => {
8
- // by model
7
+ const queryBySiteSetting = (query) => {
8
+ if (!req.site) return
9
9
  const setting = get(req, `site.setting.dobo.query.${modelName}`)
10
10
  if (isPlainObject(setting) && !isEmpty(setting)) query.$and.push(setting)
11
- return query
11
+ }
12
+
13
+ const queryByTeamSetting = (query) => {
14
+ if (!req.user) return
15
+ const q = []
16
+ for (const team of req.user.teams) {
17
+ const item = get(team, `setting.dobo.query.${modelName}`)
18
+ if (item) q.push(item)
19
+ }
20
+ if (isEmpty(q)) return
21
+ if (q.length === 1) query.$and.push(q[0])
22
+ else query.$and.push({ $or: q })
12
23
  }
13
24
 
14
25
  const model = this.app.dobo.getModel(modelName)
@@ -17,14 +28,17 @@ export async function rebuildFilter (modelName, filter, req) {
17
28
  const hasTeamId = model.hasProperty('teamId')
18
29
  const isAdmin = find(get(req, 'user.teams', []), { alias: 'administrator' }) && useAdmin.includes(get(req, 'routeOptions.config.ns'))
19
30
  const q = { $and: [] }
31
+ queryBySiteSetting(q)
32
+ queryByTeamSetting(q)
20
33
  if (!isEmpty(filter.query)) {
21
34
  if (filter.query.$and) q.$and.push(...filter.query.$and)
22
35
  else q.$and.push(filter.query)
23
36
  }
37
+ /*
24
38
  if (!(hasSiteId || hasUserId || hasTeamId)) {
25
- filter.query = queryByModel(q)
26
39
  return filter
27
40
  }
41
+ */
28
42
  if (hasSiteId) q.$and.push({ siteId: req.site.id })
29
43
  if (hasTeamId && !isAdmin) {
30
44
  const teamIds = map(req.user.teams, 'id')
@@ -33,14 +47,13 @@ export async function rebuildFilter (modelName, filter, req) {
33
47
  } else if (!isAdmin) {
34
48
  if (hasUserId) q.$and.push({ userId: req.user.id })
35
49
  }
36
- filter.query = queryByModel(q)
37
- return filter
50
+ filter.query = q
38
51
  }
39
52
 
40
53
  export async function handler (modelName, filter, options = {}) {
41
54
  const { req } = options
42
55
  if (options.noAutoFilter || !req) return
43
- filter = await rebuildFilter.call(this, modelName, filter, req)
56
+ await rebuildFilter.call(this, modelName, filter, req)
44
57
  }
45
58
 
46
59
  const doboBeforeFindRecord = {
@@ -5,7 +5,8 @@ export async function checker (modelName, id, options = {}) {
5
5
 
6
6
  const model = this.app.dobo.getModel(modelName)
7
7
  if (options.noAutoFilter || !req) return
8
- const filter = await rebuildFilter.call(this, modelName, {}, req)
8
+ const filter = {}
9
+ await rebuildFilter.call(this, modelName, filter, req)
9
10
  if (filter.query.$and) filter.query.$and.push({ id })
10
11
  else filter.query.id = id
11
12
  filter.limit = 1
@@ -1,8 +1,7 @@
1
1
  const preParsing = {
2
2
  level: 10,
3
3
  handler: async function (req, reply) {
4
- const { getHostname } = this.app.waibu
5
- req.site = await this.getSite(getHostname(req))
4
+ req.site = await this.getSite(req)
6
5
  }
7
6
  }
8
7
 
@@ -100,6 +100,7 @@
100
100
  "manageUser": "Manage User",
101
101
  "manageTeam": "Manage Team",
102
102
  "manageTeamUser": "Manage Team Member",
103
+ "manageTeamSetting": "Manage Team Setting",
103
104
  "resetUserPassword": "Reset User Password",
104
105
  "unknownUser": "User Unknown/Invalid",
105
106
  "socialMedia": "Social Media",
@@ -101,6 +101,7 @@
101
101
  "manageUser": "Kelola Pengguna",
102
102
  "manageTeam": "Kelola Tim",
103
103
  "manageTeamUser": "Kelola Anggota Tim",
104
+ "manageTeamSetting": "Kelola Setelan Tim",
104
105
  "resetUserPassword": "Reset Kata Sandi Pengguna",
105
106
  "unknownUser": "Pengguna Tidak Dikenal/Tidak Valid",
106
107
  "socialMedia": "Media Sosial",
@@ -4,11 +4,19 @@ async function teamId (opts = {}) {
4
4
  name: 'teamId',
5
5
  type: 'string',
6
6
  maxLength: 50,
7
+ required: true,
7
8
  ref: {
8
9
  site: {
9
10
  model: 'SumbaSite',
10
11
  propName: 'id',
11
- type: '1:1'
12
+ type: '1:1',
13
+ fields: ['id', 'alias', 'hostname', 'title']
14
+ },
15
+ team: {
16
+ model: 'SumbaTeam',
17
+ propName: 'id',
18
+ type: '1:1',
19
+ fields: ['id', 'name']
12
20
  }
13
21
  },
14
22
  index: true
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "properties": [
3
3
  "ns,,50,true,true",
4
- "key,,50,true,true",
4
+ "key,,255,true,true",
5
5
  "value,text"
6
6
  ],
7
7
  "indexes": [{
8
8
  "fields": ["ns", "key", "siteId"],
9
9
  "type": "unique"
10
10
  }],
11
- "features": ["sumba:siteId", "dobo:updatedAt"]
11
+ "features": [
12
+ "sumba:siteId",
13
+ "dobo:immutable",
14
+ "dobo:updatedAt"
15
+ ]
12
16
  }
@@ -3,14 +3,15 @@
3
3
  "properties": [{
4
4
  "name": "hostname",
5
5
  "type": "string",
6
- "minLength": 5,
7
6
  "maxLength": 100,
7
+ "required": true,
8
8
  "index": "unique"
9
9
  }, {
10
10
  "name": "alias",
11
11
  "type": "string",
12
12
  "maxLength": 100,
13
13
  "index": "unique",
14
+ "required": true,
14
15
  "immutable": true
15
16
  }, {
16
17
  "name": "title",
@@ -20,7 +21,6 @@
20
21
  }, {
21
22
  "name": "orgName",
22
23
  "type": "string",
23
- "minLength": 5,
24
24
  "maxLength": 100,
25
25
  "index": true
26
26
  }, {
@@ -0,0 +1,17 @@
1
+ {
2
+ "buildLevel": 5,
3
+ "properties": [
4
+ "ns,,50,true,true",
5
+ "key,,255,true,true",
6
+ "value,text"
7
+ ],
8
+ "indexes": [{
9
+ "fields": ["ns", "key", "siteId", "teamId"],
10
+ "type": "unique"
11
+ }],
12
+ "features": [
13
+ "sumba:siteId",
14
+ "dobo:updatedAt",
15
+ "sumba:teamId"
16
+ ]
17
+ }
@@ -1,19 +1,6 @@
1
1
  {
2
2
  "buildLevel": 4,
3
- "properties": [{
4
- "name": "teamId",
5
- "type": "string",
6
- "maxLength": 50,
7
- "required": true,
8
- "ref": {
9
- "team": {
10
- "model": "SumbaTeam",
11
- "propName": "id",
12
- "type": "1:1",
13
- "fields": ["id", "name"]
14
- }
15
- }
16
- }],
3
+ "properties": [],
17
4
  "indexes": [{
18
5
  "fields": ["userId", "siteId", "teamId"],
19
6
  "type": "unique"
@@ -21,6 +8,7 @@
21
8
  "features": [
22
9
  "dobo:createdAt",
23
10
  "dobo:updatedAt",
11
+ "sumba:teamId",
24
12
  "sumba:siteId",
25
13
  "sumba:userId",
26
14
  "dobo:immutable"
@@ -11,7 +11,6 @@ const auth = {
11
11
  user = await this.getUser(session.userId)
12
12
  if (user) {
13
13
  socket.join(camelCase(`user ${user.username}`))
14
- await this.mergeTeam(user, site)
15
14
  for (const team of user.teams) {
16
15
  socket.join(camelCase(`team ${team.alias}`))
17
16
  }
@@ -0,0 +1,51 @@
1
+ async function teamUser () {
2
+ return {
3
+ common: {
4
+ layout: [
5
+ { name: 'meta', fields: ['id', 'teamId', 'createdAt', 'updatedAt'] },
6
+ { name: 'general', fields: ['ns', 'key', 'value'] }
7
+ ],
8
+ calcFields: [
9
+ { name: 'team', type: 'string' }
10
+ ],
11
+ valueFormatter: {
12
+ team: (val, rec) => {
13
+ return rec._ref.team.name
14
+ }
15
+ },
16
+ widget: {
17
+ teamId: {
18
+ component: 'form-select-ext',
19
+ attr: {
20
+ remoteUrl: 'sumba.restapi:/manage/team',
21
+ remoteSearchField: 'name',
22
+ remoteLabelField: 'name',
23
+ remoteApiKey: true,
24
+ ref: 'team:name'
25
+ }
26
+ }
27
+ }
28
+ },
29
+ view: {
30
+ list: {
31
+ fields: ['team', 'ns', 'key', 'value', 'createdAt', 'updatedAt'],
32
+ stat: {
33
+ aggregate: [
34
+ { fields: ['userId'], group: 'userId', aggregate: ['count'] },
35
+ { fields: ['teamId'], group: 'teamId', aggregate: ['count'] }
36
+ ]
37
+ }
38
+ },
39
+ details: {
40
+ },
41
+ add: {
42
+ hidden: ['id', 'createdAt', 'updatedAt']
43
+ },
44
+ edit: {
45
+ readonly: ['id', 'createdAt', 'updatedAt']
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export default teamUser
@@ -25,7 +25,7 @@ async function teamUser () {
25
25
  remoteSearchField: 'username',
26
26
  remoteLabelField: 'username',
27
27
  remoteApiKey: true,
28
- rel: 'user:username'
28
+ ref: 'user:username'
29
29
  }
30
30
  },
31
31
  teamId: {
@@ -35,7 +35,7 @@ async function teamUser () {
35
35
  remoteSearchField: 'name',
36
36
  remoteLabelField: 'name',
37
37
  remoteApiKey: true,
38
- rel: 'team:name'
38
+ ref: 'team:name'
39
39
  }
40
40
  }
41
41
  }
@@ -0,0 +1,11 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'manageTeamSetting',
4
+ handler: async function (req, reply) {
5
+ const { importModule } = this.app.bajo
6
+ const crudSkel = await importModule('waibuAdmin:/lib/crud-skel.js')
7
+ return await crudSkel.call(this, 'SumbaTeamSetting', req, reply)
8
+ }
9
+ }
10
+
11
+ export default action
@@ -4,6 +4,7 @@ const profile = {
4
4
  if (!this.app.masohiMail) return await reply.view('sumba.template:/user/forgot-password.html')
5
5
  const { defaultsDeep } = this.app.lib.aneka
6
6
  const { dayjs } = this.app.lib
7
+ const { get } = this.app.lib._
7
8
  const model = this.app.dobo.getModel('SumbaUser')
8
9
  const form = defaultsDeep(req.body, {})
9
10
  let error
@@ -16,7 +17,7 @@ const profile = {
16
17
  const to = `${data.firstName} ${data.lastName} <${data.email}>`
17
18
  const subject = req.t('forgotPasswordLink')
18
19
  const options = { req, reply }
19
- const exp = req.site.setting.sumba.forgotPasswordExpDur
20
+ const exp = get(req, 'site.setting.sumba.forgotPasswordExpDur')
20
21
  data.fpToken = Buffer.from(`${data.token}:${dayjs().add(exp, 'ms').unix()}`).toString('base64')
21
22
  data._meta = { hostHeader: req.headers.host }
22
23
  const payload = { to, subject, data }
package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import path from 'path'
2
2
  import createNewSite from './lib/create-new-site.js'
3
3
  import removeSite from './lib/remove-site.js'
4
+ import getSite from './lib/get-site.js'
5
+ import getUser from './lib/get-user.js'
4
6
 
5
7
  /**
6
8
  * Plugin factory
@@ -10,6 +12,7 @@ import removeSite from './lib/remove-site.js'
10
12
  */
11
13
  async function factory (pkgName) {
12
14
  const me = this
15
+ const { getModel } = this.app.dobo
13
16
 
14
17
  /**
15
18
  * Sumba class
@@ -132,7 +135,7 @@ async function factory (pkgName) {
132
135
  }
133
136
  }
134
137
  this.unsafeUserFields = ['password']
135
- this.selfBind(['createNewSite', 'removeSite'])
138
+ this.selfBind(['createNewSite', 'removeSite', 'getSite', 'getUser'])
136
139
  }
137
140
 
138
141
  init = async () => {
@@ -187,6 +190,7 @@ async function factory (pkgName) {
187
190
  { title: 'manageUser', href: `waibuAdmin:/${prefix}/user/list` },
188
191
  { title: 'manageTeam', href: `waibuAdmin:/${prefix}/team/list` },
189
192
  { title: 'manageTeamUser', href: `waibuAdmin:/${prefix}/team-user/list` },
193
+ { title: 'manageTeamSetting', href: `waibuAdmin:/${prefix}/team-setting/list` },
190
194
  { title: 'manageDownload', href: `waibuAdmin:/${prefix}/download/list` },
191
195
  { title: '-' },
192
196
  { title: 'siteSetting', href: `waibuAdmin:/${prefix}/site-setting/list` },
@@ -200,37 +204,9 @@ async function factory (pkgName) {
200
204
  }]
201
205
  }
202
206
 
203
- getUser = async (rec, safe = true) => {
204
- const { omit, isPlainObject } = this.app.lib._
205
- let user
206
- if (isPlainObject(rec)) user = rec
207
- else {
208
- const mdl = this.app.dobo.getModel('SumbaUser')
209
- user = await mdl.getRecord(rec, { noHook: true, throwNotFound: false })
210
- }
211
- if (!user) return null
212
- return safe ? omit(user, this.unsafeUserFields) : user
213
- }
214
-
215
- mergeTeam = async (user, site) => {
216
- if (!user) return
217
- const { map, pick } = this.app.lib._
218
- user.teams = []
219
- const query = { userId: user.id, siteId: site.id }
220
- let mdl = this.app.dobo.getModel('SumbaTeamUser')
221
- const userTeam = await mdl.findAllRecord({ query })
222
- if (userTeam.length === 0) return
223
- delete query.userId
224
- query.id = { $in: map(userTeam, 'teamId') }
225
- query.status = 'ENABLED'
226
- mdl = this.app.dobo.getModel('SumbaTeam')
227
- const team = await mdl.findAllRecord({ query })
228
- if (team.length > 0) user.teams.push(...map(team, t => pick(t, ['id', 'alias'])))
229
- }
230
-
231
207
  getUserFromUsernamePassword = async (username = '', password = '', req) => {
232
208
  const { importPkg } = this.app.bajo
233
- const model = this.app.dobo.getModel('SumbaUser')
209
+ const model = getModel('SumbaUser')
234
210
  await model.validate({ username, password }, null, { partial: true, ns: ['sumba', 'dobo'], fields: ['username', 'password'] })
235
211
  const bcrypt = await importPkg('bajoExtra:bcrypt')
236
212
 
@@ -286,7 +262,7 @@ async function factory (pkgName) {
286
262
  if (!isMd5(token)) return false
287
263
  token = await hash(token)
288
264
  const query = { token }
289
- const rows = await this.app.dobo.getModel('SumbaUser').findRecord({ query }, { req, noHook: true })
265
+ const rows = await getModel('SumbaUser').findRecord({ query }, { req, noHook: true })
290
266
  if (rows.length === 0) throw this.error('invalidKey', merge({ statusCode: 401 }, payload))
291
267
  if (rows[0].status !== 'ACTIVE') throw this.error('userInactive', merge({ details: [{ field: 'status', error: 'inactive' }], statusCode: 401 }, payload))
292
268
  req.user = await getUser(rows[0])
@@ -352,7 +328,7 @@ async function factory (pkgName) {
352
328
  const decoded = await verifier(token)
353
329
  const id = decoded.payload.uid
354
330
  try {
355
- const rec = await this.app.dobo.getModel('SumbaUser').getRecord(id, { req, noHook: true })
331
+ const rec = await getModel('SumbaUser').getRecord(id, { req, noHook: true })
356
332
  if (!rec) throw this.error('invalidToken', { statusCode: 401 })
357
333
  if (rec.status !== 'ACTIVE') throw this.error('userInactive', { details: [{ field: 'status', error: 'inactive' }], statusCode: 401 })
358
334
  req.user = await getUser(rec)
@@ -405,73 +381,6 @@ async function factory (pkgName) {
405
381
  return guarded
406
382
  }
407
383
 
408
- getSite = async (hostname, useId) => {
409
- const { omit } = this.app.lib._
410
- const omitted = ['status']
411
-
412
- const mergeSetting = async (site) => {
413
- const { defaultsDeep } = this.app.lib.aneka
414
- const { parseObject, dayjs } = this.app.lib
415
- const { trim, get, filter } = this.app.lib._
416
- const defSetting = {}
417
- const nsSetting = {}
418
- const names = this.app.getAllNs()
419
- const query = {
420
- ns: { $in: names },
421
- siteId: site.id
422
- }
423
- const all = await this.app.dobo.getModel('SumbaSiteSetting').findAllRecord({ query })
424
- for (const ns of names) {
425
- nsSetting[ns] = {}
426
- defSetting[ns] = get(this, `app.${ns}.config.siteSetting`, {})
427
- const items = filter(all, { ns })
428
- for (const item of items) {
429
- let value = trim([item.value] ?? '')
430
- if (['[', '{'].includes(value[0])) {
431
- try {
432
- value = parseObject(JSON.parse(value))
433
- } catch (err) {}
434
- } else if (Number(value)) value = Number(value)
435
- else if (['true', 'false'].includes(value)) value = value === 'true'
436
- else {
437
- const dt = dayjs(value)
438
- if (dt.isValid()) value = dt.toDate()
439
- }
440
- nsSetting[ns][item.key] = value
441
- }
442
- }
443
- site.setting = defaultsDeep({}, nsSetting, defSetting)
444
- // additional fields
445
- const country = await this.app.dobo.getModel('CdbCountry').getRecord(site.country, { noHook: true })
446
- site.countryName = (country ?? {}).name ?? site.country
447
- }
448
-
449
- let site = {}
450
-
451
- const multiSite = this.config.multiSite === true ? { enabled: true, catchAll: 'default' } : this.config.multiSite
452
- if (!multiSite.enabled) {
453
- const resp = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query: { alias: 'default' } }, { noHook: true })
454
- site = omit(resp, omitted)
455
- await mergeSetting(site)
456
- return site
457
- }
458
- let query
459
- if (useId) query = { id: hostname }
460
- else query = { hostname }
461
- let row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
462
- if (!row) {
463
- if (multiSite.catchAll) {
464
- query = { alias: multiSite.catchAll === true ? 'default' : multiSite.catchAll }
465
- row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
466
- }
467
- if (!row) throw this.error('unknownSite')
468
- }
469
- if (row.status !== 'ACTIVE') throw this.error('siteInactiveInfo')
470
- site = omit(row, omitted)
471
- await mergeSetting(site)
472
- return site
473
- }
474
-
475
384
  signout = async ({ req, reply, reason }) => {
476
385
  const { runHook } = this.app.bajo
477
386
  const { getSessionId } = this.app.waibuMpa
@@ -502,9 +411,10 @@ async function factory (pkgName) {
502
411
  return reply.redirectTo(url, { query, params })
503
412
  }
504
413
 
505
- generatePassword = (req) => {
414
+ generatePassword = (req = {}) => {
415
+ const { get } = this.app.lib._
506
416
  const { generateId } = this.app.lib.aneka
507
- const cfg = req ? req.site.setting.sumba.userPassword : this.config.siteSetting.userPassword
417
+ const cfg = get(req, 'site.setting.sumba.userPassword', this.config.siteSetting.userPassword)
508
418
  let passwd = generateId()
509
419
  if (cfg.minLowercase) passwd += generateId({ pattern: 'abcdefghijklmnopqrstuvwxyz', length: cfg.minLowercase })
510
420
  if (cfg.minUppercase) passwd += generateId({ pattern: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', length: cfg.minUppercase })
@@ -537,7 +447,7 @@ async function factory (pkgName) {
537
447
  getApiKeyFromUserId = async id => {
538
448
  const { hash } = this.app.bajoExtra
539
449
  const options = { forceNoHidden: true, noHook: true, noCache: true, attachment: true, mimeType: true }
540
- const resp = await this.app.dobo.getModel('SumbaUser').getRecord(id, options)
450
+ const resp = await getModel('SumbaUser').getRecord(id, options)
541
451
  return await hash(resp.salt)
542
452
  }
543
453
 
@@ -583,6 +493,8 @@ async function factory (pkgName) {
583
493
 
584
494
  createNewSite = createNewSite
585
495
  removeSite = removeSite
496
+ getSite = getSite
497
+ getUser = getUser
586
498
  }
587
499
 
588
500
  return Sumba
package/lib/check-team.js CHANGED
@@ -3,7 +3,6 @@ import { pathsToCheck } from './check-user-id.js'
3
3
  async function checkTeam (req, reply, source) {
4
4
  if (!req.user) return
5
5
  const { map } = this.app.lib._
6
- await this.mergeTeam(req.user, req.site)
7
6
  const paths = pathsToCheck.call(this, req, true)
8
7
  const teams = map(req.user.teams, 'alias')
9
8
  const match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamRoutes })
@@ -0,0 +1,60 @@
1
+ import { parseNsSettings } from './util.js'
2
+
3
+ async function getSite (req) {
4
+ const { runHook } = this.app.bajo
5
+ const { omit, isPlainObject } = this.app.lib._
6
+ const { getHostname } = this.app.waibu
7
+ const omitted = ['status']
8
+
9
+ await runHook(`${this.ns}:beforeGetSite`, req)
10
+ const mergeSetting = async (site) => {
11
+ const { defaultsDeep, isSet } = this.app.lib.aneka
12
+ const { get, filter } = this.app.lib._
13
+ const defSetting = {}
14
+ const nsSetting = {}
15
+ const names = this.app.getAllNs()
16
+ const query = {
17
+ ns: { $in: names },
18
+ siteId: site.id
19
+ }
20
+ const all = await this.app.dobo.getModel('SumbaSiteSetting').findAllRecord({ query })
21
+ for (const ns of names) {
22
+ const item = get(this, `app.${ns}.config.siteSetting`)
23
+ if (isSet(item)) defSetting[ns] = item
24
+ const items = filter(all, { ns })
25
+ parseNsSettings.call(this, ns, nsSetting, items)
26
+ }
27
+ site.setting = defaultsDeep({}, nsSetting, defSetting)
28
+ // additional fields
29
+ const country = await this.app.dobo.getModel('CdbCountry').getRecord(site.country, { noHook: true })
30
+ site.countryName = (country ?? {}).name ?? site.country
31
+ await runHook(`${this.ns}:afterGetSite`, req, site)
32
+ }
33
+
34
+ let site = {}
35
+
36
+ const multiSite = this.config.multiSite === true ? { enabled: true, catchAll: 'default' } : this.config.multiSite
37
+ if (!multiSite.enabled) {
38
+ const resp = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query: { alias: 'default' } }, { noHook: true })
39
+ site = omit(resp, omitted)
40
+ await mergeSetting(site)
41
+ return site
42
+ }
43
+ let query
44
+ if (!isPlainObject(req)) query = { id: req }
45
+ else query = { hostname: getHostname(req) }
46
+ let row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
47
+ if (!row) {
48
+ if (multiSite.catchAll) {
49
+ query = { alias: multiSite.catchAll === true ? 'default' : multiSite.catchAll }
50
+ row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
51
+ }
52
+ if (!row) throw this.error('unknownSite')
53
+ }
54
+ if (row.status !== 'ACTIVE') throw this.error('siteInactiveInfo')
55
+ site = omit(row, omitted)
56
+ await mergeSetting(site)
57
+ return site
58
+ }
59
+
60
+ export default getSite
@@ -0,0 +1,50 @@
1
+ import { parseNsSettings } from './util.js'
2
+
3
+ async function getUser (rec, safe = true) {
4
+ const { runHook } = this.app.bajo
5
+ const { map, pick, omit, isPlainObject } = this.app.lib._
6
+ const { getModel } = this.app.dobo
7
+ await runHook(`${this.ns}:beforeGetUser`, rec, safe)
8
+ let user
9
+ if (!isPlainObject(rec)) {
10
+ const mdl = getModel('SumbaUser')
11
+ user = await mdl.getRecord(rec, { noHook: true, throwNotFound: false })
12
+ } else {
13
+ user = rec
14
+ }
15
+ if (!user) return null
16
+ // merge teams
17
+ user.teams = []
18
+ const query = { userId: user.id, siteId: user.siteId }
19
+ let mdl = getModel('SumbaTeamUser')
20
+ const userTeam = await mdl.findAllRecord({ query })
21
+ if (userTeam.length === 0) return
22
+ delete query.userId
23
+ query.id = { $in: map(userTeam, 'teamId') }
24
+ query.status = 'ENABLED'
25
+ mdl = getModel('SumbaTeam')
26
+ const teams = await mdl.findAllRecord({ query })
27
+ if (teams.length > 0) {
28
+ // setting
29
+ delete query.id
30
+ delete query.status
31
+ query.siteId = user.siteId
32
+ query.teamId = { $in: teams.map(t => t.id + '') }
33
+ mdl = getModel('SumbaTeamSetting')
34
+ const items = await mdl.findAllRecord({ query })
35
+ for (const team of teams) {
36
+ const names = map(items, 'ns')
37
+ const item = pick(team, ['id', 'alias'])
38
+ item.setting = {}
39
+ for (const ns of names) {
40
+ parseNsSettings.call(this, ns, item.setting, items.filter(s => s.teamId === (team.id + '')))
41
+ }
42
+ user.teams.push(item)
43
+ }
44
+ }
45
+ user = safe ? omit(user, this.unsafeUserFields) : user
46
+ await runHook(`${this.ns}:afterGetUser`, rec, safe, user)
47
+ return user
48
+ }
49
+
50
+ export default getUser
@@ -1,6 +1,7 @@
1
1
  import { joiPasswordExtendCore } from 'joi-password'
2
2
 
3
- async function passwordRule (req) {
3
+ async function passwordRule (req = {}) {
4
+ const { get } = this.app.lib._
4
5
  const { importPkg } = this.app.bajo
5
6
  const joi = await importPkg('dobo:joi')
6
7
  const joiPassword = joi.extend(joiPasswordExtendCore)
@@ -9,7 +10,7 @@ async function passwordRule (req) {
9
10
  .min(8)
10
11
  .max(100)
11
12
  .required()
12
- const cfg = req ? req.site.setting.sumba.userPassword : this.config.siteSetting.userPassword
13
+ const cfg = get(req, 'site.setting.sumba.userPassword', this.config.siteSetting.userPassword)
13
14
  if (cfg.minUppercase) password = password.minOfUppercase(cfg.minUppercase)
14
15
  if (cfg.minLowercase) password = password.minOfLowercase(cfg.minLowercase)
15
16
  if (cfg.minSpecialChar) password = password.minOfSpecialCharacters(cfg.minSpecialChar)
package/lib/util.js ADDED
@@ -0,0 +1,21 @@
1
+ export function parseNsSettings (ns, setting, items) {
2
+ const { trim, set, isPlainObject, isArray, isEmpty } = this.app.lib._
3
+ const { parseObject, dayjs } = this.app.lib
4
+
5
+ for (const item of items) {
6
+ let value = trim([item.value] ?? '')
7
+ if (['[', '{'].includes(value[0])) {
8
+ try {
9
+ value = parseObject(JSON.parse(value))
10
+ } catch (err) {}
11
+ } else if (Number(value)) value = Number(value)
12
+ else if (['true', 'false'].includes(value)) value = value === 'true'
13
+ else if (item.key.endsWith('$in')) value = value.split(',').map(v => v.trim())
14
+ else {
15
+ const dt = dayjs(value)
16
+ if (dt.isValid()) value = dt.toDate()
17
+ }
18
+ if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
19
+ set(setting, `${ns}.${item.key}`, value)
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.9.0",
3
+ "version": "2.11.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,16 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-03-22
4
+
5
+ - [2.11.0] Add ```Team Setting``` feature
6
+ - [2.11.0] Rewrite ```getUser()```
7
+ - [2.11.0] Rewrite ```getSite()```
8
+ - [2.11.0] Bug fix in model reference not displayed correctly on ```Details View```
9
+
10
+ ## 2026-03-13
11
+
12
+ - [2.10.0] ```getSite()``` now accept object & array based on their keys to
13
+
3
14
  ## 2026-03-12
4
15
 
5
16
  - [2.9.0] Add ability to restrict/filter dobo's records through site setting