sumba 2.23.0 → 2.24.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 (41) hide show
  1. package/extend/bajo/hook/bajo.extend@after-read-config.js +13 -0
  2. package/extend/bajo/hook/dobo.sumba-route-guard@after-transaction.js +6 -0
  3. package/extend/bajo/hook/dobo@before-count-record.js +3 -3
  4. package/extend/bajo/hook/dobo@before-driver-create-record.js +17 -0
  5. package/extend/bajo/hook/dobo@before-driver-find-all-record.js +13 -0
  6. package/extend/bajo/hook/dobo@before-driver-find-record.js +96 -0
  7. package/extend/bajo/hook/dobo@before-driver-get-record.js +22 -0
  8. package/extend/bajo/hook/dobo@before-driver-remove-record.js +10 -0
  9. package/extend/bajo/hook/dobo@before-driver-update-record.js +10 -0
  10. package/extend/bajo/hook/waibu-mpa@pre-parsing.js +5 -4
  11. package/extend/bajo/hook/waibu-rest-api@pre-parsing.js +5 -4
  12. package/extend/bajo/hook/waibu-static@pre-parsing.js +5 -4
  13. package/extend/bajo/intl/en-US.json +7 -2
  14. package/extend/bajo/intl/id.json +14 -2
  15. package/extend/dobo/feature/team-ids.js +20 -0
  16. package/extend/dobo/fixture/route-guard.js +2 -2
  17. package/extend/dobo/model/attrib-guard.js +36 -0
  18. package/extend/dobo/model/model-guard.js +37 -0
  19. package/extend/dobo/model/route-guard.js +13 -28
  20. package/extend/waibuDb/schema/attrib-guard.js +15 -0
  21. package/extend/waibuDb/schema/model-guard.js +15 -0
  22. package/extend/waibuDb/schema/route-guard.js +1 -1
  23. package/extend/waibuDb/schema/user.js +19 -26
  24. package/extend/waibuMpa/extend/waibuAdmin/route/all-sites/@action.js +1 -1
  25. package/extend/waibuMpa/extend/waibuAdmin/route/attrib-guard/@action.js +11 -0
  26. package/extend/waibuMpa/extend/waibuAdmin/route/cache/@action.js +1 -1
  27. package/extend/waibuMpa/extend/waibuAdmin/route/model-guard/@action.js +11 -0
  28. package/extend/waibuMpa/extend/waibuAdmin/route/reset-user-password.js +2 -1
  29. package/extend/waibuMpa/extend/waibuAdmin/route/session/@action.js +1 -1
  30. package/index.js +20 -13
  31. package/lib/get-user.js +4 -4
  32. package/lib/util.js +21 -18
  33. package/package.json +1 -1
  34. package/wiki/CHANGES.md +6 -0
  35. package/extend/bajo/hook/dobo.sumba-team-guard@after-action.js +0 -6
  36. package/extend/bajo/hook/dobo.sumba-user-guard@after-action.js +0 -6
  37. package/extend/bajo/hook/dobo@before-create-record.js +0 -17
  38. package/extend/bajo/hook/dobo@before-find-record.js +0 -63
  39. package/extend/bajo/hook/dobo@before-get-record.js +0 -23
  40. package/extend/bajo/hook/dobo@before-remove-record.js +0 -10
  41. package/extend/bajo/hook/dobo@before-update-record.js +0 -10
@@ -0,0 +1,13 @@
1
+ import path from 'path'
2
+
3
+ async function afterReadConfig (file, result, options) {
4
+ const base = path.basename(file, path.extname(file))
5
+ // rewrite fixtures
6
+ if (!(base === 'route-guard' && Array.isArray(result) && file.includes('/fixture/'))) return
7
+ for (const res of result) {
8
+ if (res.path.slice(0, 2) === ':/') res.path = options.sourceNs + res.path
9
+ res.path = res.path.replaceAll('{ns}', options.sourceNs)
10
+ }
11
+ }
12
+
13
+ export default afterReadConfig
@@ -0,0 +1,6 @@
1
+ async function afterTransaction (action, ...args) {
2
+ if (!['createRecord', 'updateRecord', 'removeRecord'].includes(action)) return
3
+ await this.getRouteGuards(true)
4
+ }
5
+
6
+ export default afterTransaction
@@ -1,8 +1,8 @@
1
- import { handler } from './dobo@before-find-record.js'
1
+ import { handler } from './dobo@before-driver-find-record.js'
2
2
 
3
- const doboBeforeCountRecord = {
3
+ const doboBeforeDriverCountRecord = {
4
4
  level: 1000,
5
5
  handler
6
6
  }
7
7
 
8
- export default doboBeforeCountRecord
8
+ export default doboBeforeDriverCountRecord
@@ -0,0 +1,17 @@
1
+ const doboBeforeDriverCreateRecord = {
2
+ level: 1000,
3
+ handler: async function (model, body, options = {}) {
4
+ const { get } = this.app.lib._
5
+ const { isSet } = this.app.lib.aneka
6
+ const { req } = options
7
+ if (options.noAutoFilter || !req || get(req, 'routeOptions.config.crossSite')) return
8
+ const item = { siteId: 'site.id', userId: 'user.id' }
9
+ for (const i in item) {
10
+ const rec = get(req, item[i])
11
+ const field = model.getProperty(i)
12
+ if (rec && field && !isSet(body[i])) body[i] = field.type === 'string' ? (rec + '') : rec
13
+ }
14
+ }
15
+ }
16
+
17
+ export default doboBeforeDriverCreateRecord
@@ -0,0 +1,13 @@
1
+ import { rebuildFilter } from './dobo@before-driver-find-record.js'
2
+
3
+ const doboBeforeDriverFindAllRecord = {
4
+ level: 1000,
5
+ handler: async function (model, filter, options) {
6
+ const { req } = options
7
+ const { isEmpty } = this.app.lib._
8
+ if (options.noAutoFilter || !req || isEmpty(req.site)) return
9
+ await rebuildFilter.call(this, model, filter, options)
10
+ }
11
+ }
12
+
13
+ export default doboBeforeDriverFindAllRecord
@@ -0,0 +1,96 @@
1
+ async function applyModelGuard ({ model, q, teamIds, options }) {
2
+ const { isEmpty, get, set, uniq } = this.app.lib._
3
+ const { req } = options
4
+ const { getModel } = this.app.dobo
5
+
6
+ const guards = []
7
+ const query = { status: 'ACTIVE', siteId: req.site.id + '' }
8
+ const results = await getModel('SumbaModelGuard').findAllRecord({ query }, { noMagic: true, dataOnly: true, noDriverHook: true })
9
+ const item = {}
10
+
11
+ function add (res) {
12
+ item[res.column].push(...res.value)
13
+ }
14
+
15
+ for (const res of results) {
16
+ res.teamIds = res.teamIds ?? []
17
+ if (!(res.models ?? []).includes(model.name) || isEmpty(res.value)) continue
18
+ item[res.column] = item[res.column] ?? []
19
+ if (teamIds.length > 0) {
20
+ for (const id of res.teamIds) {
21
+ if (teamIds.includes(id)) add(res)
22
+ }
23
+ }
24
+ }
25
+ for (const key in item) {
26
+ item[key] = uniq(item[key])
27
+ if (item[key].length === 0) continue
28
+ guards.push(set({}, key, { $in: item[key] }))
29
+ }
30
+
31
+ const allowEmpty = get(this, `config.dobo.model.${model.name}.allowEmptyQuery`, true)
32
+ if (guards.length === 0 && !allowEmpty) throw this.error('_emptyColumnQuery') // signal driver to about with no result immediately
33
+ q.$and.push(...guards)
34
+ }
35
+
36
+ export async function applyAttribGuard ({ model, teamIds, options }) {
37
+ const { getModel } = this.app.dobo
38
+ const { uniq } = this.app.lib._
39
+ const { req } = options
40
+
41
+ const query = { status: 'ACTIVE', siteId: req.site.id + '' }
42
+ const results = await getModel('SumbaAttribGuard').findAllRecord({ query }, { noMagic: true, dataOnly: true, noDriverHook: true })
43
+ const result = results.find(item => (item.models ?? []).includes(model.name))
44
+ if (!result) return
45
+ options.hidden = options.hidden ?? []
46
+ for (const id of result.teamIds ?? []) {
47
+ if (teamIds.includes(id)) options.hidden.push(...(result.hiddenCols ?? []))
48
+ }
49
+ options.hidden = uniq(options.hidden)
50
+ }
51
+
52
+ export async function rebuildFilter (model, filter = {}, options = {}) {
53
+ const { isEmpty, get } = this.app.lib._
54
+ const { req } = options
55
+ const hasSiteId = model.hasProperty('siteId')
56
+ const hasUserId = model.hasProperty('userId')
57
+ const hasTeamId = model.hasProperty('teamId')
58
+ const teams = get(req, 'user.teams', [])
59
+ const teamIds = teams.map(team => team.id + '')
60
+ const aliases = teams.map(team => team.alias)
61
+ const q = { $and: [] }
62
+
63
+ filter.query = filter.query ?? {}
64
+ if (!isEmpty(filter.query)) {
65
+ if (filter.query.$and) q.$and.push(...filter.query.$and)
66
+ else q.$and.push(filter.query)
67
+ }
68
+ if (req.routeOptions.config.crossSite) return
69
+ if (hasSiteId) q.$and.push({ siteId: req.site.id + '' })
70
+ if (aliases.includes('administrator')) {
71
+ filter.query = q
72
+ return
73
+ }
74
+
75
+ if (hasTeamId) {
76
+ if (hasUserId) q.$and.push({ $or: [{ teamId: { $in: teamIds } }, { userId: req.user.id + '' }] })
77
+ else q.$and.push({ teamId: { $in: teamIds } })
78
+ } else if (hasUserId) q.$and.push({ userId: req.user.id + '' })
79
+
80
+ await applyModelGuard.call(this, { model, q, teamIds, options })
81
+ await applyAttribGuard.call(this, { model, teamIds, options })
82
+ filter.query = q
83
+ }
84
+
85
+ export async function handler (model, filter, options = {}) {
86
+ const { isEmpty } = this.app.lib._
87
+ if (options.noAutoFilter || !options.req || isEmpty((options.req ?? {}).site)) return
88
+ await rebuildFilter.call(this, model, filter, options)
89
+ }
90
+
91
+ const doboBeforeDriverFindRecord = {
92
+ level: 1000,
93
+ handler
94
+ }
95
+
96
+ export default doboBeforeDriverFindRecord
@@ -0,0 +1,22 @@
1
+ import { rebuildFilter } from './dobo@before-driver-find-record.js'
2
+
3
+ export async function checker (model, id, options = {}) {
4
+ const { req } = options
5
+
6
+ if (options.noAutoFilter || !req) return
7
+ const filter = {}
8
+ await rebuildFilter.call(this, model, filter, options)
9
+ if (filter.query.$and) filter.query.$and.push({ id })
10
+ else filter.query.id = id
11
+ const row = await model.findOneRecord(filter, { count: false })
12
+ if (!row) throw this.app.dobo.error('recordNotFound%s%s', id, this.name, { statusCode: 404 })
13
+ }
14
+
15
+ const doboBeforeDriverGetRecord = {
16
+ level: 1000,
17
+ handler: async function (model, id, options) {
18
+ await checker.call(this, model, id, options)
19
+ }
20
+ }
21
+
22
+ export default doboBeforeDriverGetRecord
@@ -0,0 +1,10 @@
1
+ import { checker } from './dobo@before-driver-get-record.js'
2
+
3
+ const doboBeforeDriverRemoveRecord = {
4
+ level: 1000,
5
+ handler: async function (model, id, options = {}) {
6
+ await checker.call(this, model, id, options.req)
7
+ }
8
+ }
9
+
10
+ export default doboBeforeDriverRemoveRecord
@@ -0,0 +1,10 @@
1
+ import { checker } from './dobo@before-driver-get-record.js'
2
+
3
+ const doboBeforeDriverUpdateRecord = {
4
+ level: 1000,
5
+ handler: async function (model, id, body, options = {}) {
6
+ await checker.call(this, model, id, options)
7
+ }
8
+ }
9
+
10
+ export default doboBeforeDriverUpdateRecord
@@ -1,13 +1,14 @@
1
- import { checkUserId, checkTeam, checkTheme, checkIconset, checkInterSite } from '../../../lib/util.js'
1
+ import { checkUserId, checkTeam, checkTheme, checkIconset, checkCrossSite } from '../../../lib/util.js'
2
2
 
3
3
  const preParsing = {
4
4
  level: 10,
5
5
  handler: async function (req, reply) {
6
6
  await checkTheme.call(this, req, reply)
7
7
  await checkIconset.call(this, req, reply)
8
- if (!await checkUserId.call(this, req, reply, 'waibuMpa')) return
9
- if (!await checkTeam.call(this, req, reply, 'waibuMpa')) return
10
- await checkInterSite.call(this, req, reply)
8
+ const secure = await checkUserId.call(this, req, reply, 'waibuMpa')
9
+ if (!secure) return
10
+ await checkTeam.call(this, req, reply, secure)
11
+ await checkCrossSite.call(this, req, reply)
11
12
  }
12
13
  }
13
14
 
@@ -1,11 +1,12 @@
1
- import { checkUserId, checkTeam, checkInterSite } from '../../../lib/util.js'
1
+ import { checkUserId, checkTeam, checkCrossSite } from '../../../lib/util.js'
2
2
 
3
3
  const preParsing = {
4
4
  level: 10,
5
5
  handler: async function (req, reply) {
6
- if (!await checkUserId.call(this, req, reply, 'waibuRestApi')) return
7
- if (!await checkTeam.call(this, req, reply, 'waibuRestApi')) return
8
- await checkInterSite.call(this, req, reply)
6
+ const secure = await checkUserId.call(this, req, reply, 'waibuRestApi')
7
+ if (!secure) return
8
+ await checkTeam.call(this, req, reply, secure)
9
+ await checkCrossSite.call(this, req, reply)
9
10
  }
10
11
  }
11
12
 
@@ -1,11 +1,12 @@
1
- import { checkUserId, checkTeam, checkInterSite } from '../../../lib/util.js'
1
+ import { checkUserId, checkTeam, checkCrossSite } from '../../../lib/util.js'
2
2
 
3
3
  const preParsing = {
4
4
  level: 10,
5
5
  handler: async function (req, reply) {
6
- if (!await checkUserId.call(this, req, reply, 'waibuStatic')) return
7
- if (!await checkTeam.call(this, req, reply, 'waibuStatic')) return
8
- await checkInterSite.call(this, req, reply)
6
+ const secure = await checkUserId.call(this, req, reply, 'waibuStatic')
7
+ if (!secure) return
8
+ await checkTeam.call(this, req, reply, secure)
9
+ await checkCrossSite.call(this, req, reply)
9
10
  }
10
11
  }
11
12
 
@@ -132,7 +132,9 @@
132
132
  "statusClosed": "Closed",
133
133
  "allSites": "All Sites",
134
134
  "permission": "Permission",
135
- "routeGuard": "Route Guard",
135
+ "routeGuard": "Route",
136
+ "modelGuard": "Model",
137
+ "attribGuard": "Attribute",
136
138
  "field": {
137
139
  "currentPassword": "Current Password",
138
140
  "newPassword": "New Password",
@@ -161,7 +163,10 @@
161
163
  "inverse": "Inverse?",
162
164
  "anonymous": "Anonymous?",
163
165
  "methods": "Methods",
164
- "teams": "Teams"
166
+ "teamIds": "Teams",
167
+ "column": "Column",
168
+ "hiddenCols": "Hidden Columns",
169
+ "models": "Models"
165
170
  },
166
171
  "validation": {
167
172
  "password": {
@@ -137,6 +137,10 @@
137
137
  "statusOpen": "Terbuka",
138
138
  "statusClosed": "Tertutup",
139
139
  "allSites": "Semua Situs",
140
+ "permission": "Permisi",
141
+ "routeGuard": "Rute",
142
+ "modelGuard": "Model",
143
+ "attribGuard": "Atribut",
140
144
  "field": {
141
145
  "currentPassword": "Kata Sandi Saat Ini",
142
146
  "newPassword": "Kata Sandi Baru",
@@ -159,8 +163,16 @@
159
163
  "user": "Pengguna",
160
164
  "team": "Tim",
161
165
  "ns": "Ruang Nama Modul",
162
- "value": "Value",
163
- "notes": "Catatan"
166
+ "value": "Nilai",
167
+ "notes": "Catatan",
168
+ "path": "Jejak",
169
+ "inverse": "Inverse?",
170
+ "anonymous": "Anonim?",
171
+ "methods": "Metode",
172
+ "teamIds": "Tim",
173
+ "column": "Kolom",
174
+ "hideCols": "Sembunyikan Kolom",
175
+ "models": "Model"
164
176
  },
165
177
  "validation": {
166
178
  "password": {
@@ -0,0 +1,20 @@
1
+ async function teamIds (opts = {}) {
2
+ return {
3
+ properties: [{
4
+ name: 'teamIds',
5
+ type: 'array',
6
+ default: [],
7
+ ref: {
8
+ teamIds: {
9
+ model: 'SumbaTeam',
10
+ field: 'id',
11
+ searchField: 'name',
12
+ fields: ['id', 'alias', 'name']
13
+ }
14
+ },
15
+ index: true
16
+ }]
17
+ }
18
+ }
19
+
20
+ export default teamIds
@@ -10,7 +10,7 @@ const routes = [
10
10
  '~sumba.restapi:/user/access-token/**/*'
11
11
  ]
12
12
 
13
- async function userGuard () {
13
+ async function routeGuard () {
14
14
  return routes.map(r => {
15
15
  const anonymous = r[0] === '~'
16
16
  return {
@@ -23,4 +23,4 @@ async function userGuard () {
23
23
  })
24
24
  }
25
25
 
26
- export default userGuard
26
+ export default routeGuard
@@ -0,0 +1,36 @@
1
+ async function routeGuard () {
2
+ return {
3
+ properties: [
4
+ {
5
+ name: 'models',
6
+ type: 'array',
7
+ required: true
8
+ },
9
+ 'hiddenCols,array',
10
+ 'teamIds,sumba:teamIds'
11
+ ],
12
+ indexes: [{
13
+ type: 'unique',
14
+ fields: ['models', 'siteId']
15
+ }],
16
+ features: [
17
+ {
18
+ name: 'sumba:status',
19
+ values: ['ACTIVE', 'INACTIVE'],
20
+ default: 'ACTIVE'
21
+ },
22
+ 'dobo:immutable',
23
+ 'dobo:updatedAt',
24
+ 'sumba:siteId'
25
+ ],
26
+ options: {
27
+ attachment: false
28
+ },
29
+ buildEnd: async function (model) {
30
+ const prop = model.properties.find(prop => prop.name === 'models')
31
+ prop.values = this.app.dobo.models.map(model => model.name).sort().map(item => ({ value: item, text: item }))
32
+ }
33
+ }
34
+ }
35
+
36
+ export default routeGuard
@@ -0,0 +1,37 @@
1
+ async function routeGuard () {
2
+ return {
3
+ properties: [
4
+ {
5
+ name: 'models',
6
+ type: 'array',
7
+ required: true
8
+ },
9
+ 'column,,50,true,true',
10
+ 'value,array',
11
+ 'teamIds,sumba:teamIds'
12
+ ],
13
+ indexes: [{
14
+ type: 'unique',
15
+ fields: ['models', 'column', 'siteId']
16
+ }],
17
+ features: [
18
+ {
19
+ name: 'sumba:status',
20
+ values: ['ACTIVE', 'INACTIVE'],
21
+ default: 'ACTIVE'
22
+ },
23
+ 'dobo:immutable',
24
+ 'dobo:updatedAt',
25
+ 'sumba:siteId'
26
+ ],
27
+ options: {
28
+ attachment: false
29
+ },
30
+ buildEnd: async function (model) {
31
+ const prop = model.properties.find(prop => prop.name === 'models')
32
+ prop.values = this.app.dobo.models.map(model => model.name).sort().map(item => ({ value: item, text: item }))
33
+ }
34
+ }
35
+ }
36
+
37
+ export default routeGuard
@@ -1,9 +1,20 @@
1
1
  async function routeGuard () {
2
2
  return {
3
- connection: 'memory',
4
3
  properties: [
5
4
  'path,,255,true,true',
6
5
  {
6
+ name: 'methods',
7
+ type: 'array',
8
+ required: true,
9
+ default: ['GET', 'POST', 'UPDATE', 'DELETE'],
10
+ values: ['GET', 'POST', 'UPDATE', 'DELETE']
11
+ },
12
+ 'teamIds,sumba:teamIds',
13
+ {
14
+ name: 'weight',
15
+ type: 'smallint',
16
+ default: 0
17
+ }, {
7
18
  name: 'inverse',
8
19
  type: 'boolean',
9
20
  required: true,
@@ -13,28 +24,6 @@ async function routeGuard () {
13
24
  type: 'boolean',
14
25
  required: true,
15
26
  default: false
16
- }, {
17
- name: 'methods',
18
- type: 'array',
19
- required: true,
20
- default: ['GET', 'POST', 'UPDATE', 'DELETE'],
21
- values: ['GET', 'POST', 'UPDATE', 'DELETE']
22
- }, {
23
- name: 'teams',
24
- type: 'array',
25
- default: [],
26
- ref: {
27
- teams: {
28
- model: 'SumbaTeam',
29
- field: 'alias',
30
- searchField: 'name',
31
- fields: ['alias', 'name']
32
- }
33
- }
34
- }, {
35
- name: 'weight',
36
- type: 'smallint',
37
- default: 0
38
27
  }
39
28
  ],
40
29
  features: [
@@ -43,16 +32,12 @@ async function routeGuard () {
43
32
  values: ['ACTIVE', 'INACTIVE'],
44
33
  default: 'ACTIVE'
45
34
  },
46
- {
47
- name: 'dobo:unique',
48
- fields: ['path', 'anonymous', 'methods', 'teams', 'inverse', 'weight', 'status', 'siteId']
49
- },
50
35
  'dobo:immutable',
51
36
  'dobo:updatedAt',
52
37
  'sumba:siteId'
53
38
  ],
54
39
  options: {
55
- persistence: false
40
+ attachment: false
56
41
  }
57
42
  }
58
43
  }
@@ -0,0 +1,15 @@
1
+ async function attribGuard () {
2
+ return {
3
+ common: {
4
+ widget: {
5
+ teamIds: {
6
+ attr: {
7
+ refUrl: 'waibuAdmin:/{prefix}/team/details?id={id}'
8
+ }
9
+ }
10
+ }
11
+ }
12
+ }
13
+ }
14
+
15
+ export default attribGuard
@@ -0,0 +1,15 @@
1
+ async function modelGuard () {
2
+ return {
3
+ common: {
4
+ widget: {
5
+ teamIds: {
6
+ attr: {
7
+ refUrl: 'waibuAdmin:/{prefix}/team/details?id={id}'
8
+ }
9
+ }
10
+ }
11
+ }
12
+ }
13
+ }
14
+
15
+ export default modelGuard
@@ -2,7 +2,7 @@ async function routeGuard () {
2
2
  return {
3
3
  common: {
4
4
  widget: {
5
- teams: {
5
+ teamIds: {
6
6
  attr: {
7
7
  refUrl: 'waibuAdmin:/{prefix}/team/details?id={id}'
8
8
  }
@@ -1,9 +1,23 @@
1
1
  async function user ({ req } = {}) {
2
+ const { merge } = this.app.lib._
3
+ const details = {
4
+ forceVisible: ['password', 'token'],
5
+ format: {
6
+ password: function (val, rec) {
7
+ return `<a href="waibuAdmin:/site/reset-user-password?username=${rec.username}">${req.t('resetPassword')}</a>`
8
+ }
9
+ },
10
+ widget: {
11
+ password: {
12
+ component: 'form-plaintext'
13
+ }
14
+ }
15
+ }
2
16
  return {
3
17
  common: {
4
18
  layout: [
5
19
  { 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', 'apiKey:6'] },
20
+ { name: 'account', fields: ['username:3', 'email:3', 'provider:3', 'password:3', 'firstName:3', 'lastName:3'] },
7
21
  { 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
22
  { name: 'socialMedia', fields: ['socX:3-md 6-sm', 'socInstagram:3-md 6-sm', 'socFacebook:3-md 6-sm', 'socLinkedIn:3-md 6-sm'] }
9
23
  ],
@@ -19,7 +33,7 @@ async function user ({ req } = {}) {
19
33
  sort: 'username:1',
20
34
  limit: 10
21
35
  },
22
- fields: ['createdAt', 'status', 'username', 'provider', 'email', 'firstName', 'lastName', 'city', 'zipCode', 'provinceState', 'country', 'phone', 'apiKey'],
36
+ fields: ['createdAt', 'status', 'username', 'provider', 'email', 'firstName', 'lastName', 'city', 'zipCode', 'provinceState', 'country', 'phone'],
23
37
  stat: {
24
38
  aggregate: [
25
39
  { fields: ['status'], group: 'status', aggregate: ['count'] },
@@ -28,35 +42,14 @@ async function user ({ req } = {}) {
28
42
  ]
29
43
  }
30
44
  },
31
- details: {
32
- forceVisible: ['password', 'token'],
33
- widget: {
34
- password: {
35
- component: 'form-plaintext',
36
- attr: {
37
- href: 'waibuAdmin:/site/reset-user-password?username={username}',
38
- value: req.t('resetPassword')
39
- }
40
- }
41
- }
42
- },
45
+ details,
43
46
  add: {
44
47
  forceVisible: ['password'],
45
48
  hidden: ['id', 'createdAt', 'updatedAt', 'provider']
46
49
  },
47
- edit: {
48
- forceVisible: ['password', 'token'],
49
- widget: {
50
- password: {
51
- component: 'form-plaintext',
52
- attr: {
53
- href: 'waibuAdmin:/site/reset-user-password?username={username}',
54
- value: req.t('resetPassword')
55
- }
56
- }
57
- },
50
+ edit: merge({}, details, {
58
51
  readonly: ['id', 'createdAt', 'updatedAt', 'username', 'provider']
59
- }
52
+ })
60
53
  }
61
54
  }
62
55
  }
@@ -1,7 +1,7 @@
1
1
  const action = {
2
2
  method: ['GET', 'POST'],
3
3
  title: 'manageAllSite',
4
- interSite: true,
4
+ crossSite: true,
5
5
  handler: async function (req, reply) {
6
6
  const { importModule } = this.app.bajo
7
7
  const crudSkel = await importModule('waibuAdmin:/lib/crud-skel.js')
@@ -0,0 +1,11 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'attribGuard',
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, 'SumbaAttribGuard', req, reply)
8
+ }
9
+ }
10
+
11
+ export default action
@@ -1,7 +1,7 @@
1
1
  const action = {
2
2
  method: ['GET', 'POST'],
3
3
  title: 'cacheStorage',
4
- interSite: true,
4
+ crossSite: true,
5
5
  handler: async function (req, reply) {
6
6
  if (!this.app.bajoCache) throw this.error('_notFound')
7
7
  const { importModule } = this.app.bajo
@@ -0,0 +1,11 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'modelGuard',
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, 'SumbaModelGuard', req, reply)
8
+ }
9
+ }
10
+
11
+ export default action
@@ -4,13 +4,14 @@ const resetUserPassword = {
4
4
  handler: async function (req, reply) {
5
5
  const { importPkg } = this.app.bajo
6
6
  const { defaultsDeep } = this.app.lib.aneka
7
+ const { passwordRule } = this.app.sumba
7
8
  const Joi = await importPkg('dobo:joi')
8
9
  const model = this.app.dobo.getModel('SumbaUser')
9
10
  const form = defaultsDeep(req.body, { username: req.query.username })
10
11
  let error
11
12
  if (req.method === 'POST') {
12
13
  try {
13
- const password = await this.passwordRule(req)
14
+ const password = await passwordRule(req)
14
15
  const schema = Joi.object({
15
16
  username: Joi.string().max(50).required(),
16
17
  password,
@@ -1,7 +1,7 @@
1
1
  const action = {
2
2
  method: ['GET', 'POST'],
3
3
  title: 'userSession',
4
- interSite: true,
4
+ crossSite: true,
5
5
  handler: async function (req, reply) {
6
6
  const { importModule } = this.app.bajo
7
7
  const crudSkel = await importModule('waibuAdmin:/lib/crud-skel.js')
package/index.js CHANGED
@@ -32,11 +32,14 @@ async function factory (pkgName) {
32
32
  super(pkgName, me.app)
33
33
  this.config = {
34
34
  multiSite: cloneDeep(defMultiSite),
35
- interSiteAdmins: [],
35
+ crossSiteAdmins: [],
36
36
  waibu: {
37
37
  title: 'site',
38
38
  prefix: 'site'
39
39
  },
40
+ dobo: {
41
+ model: {}
42
+ },
40
43
  waibuMpa: {
41
44
  home: 'sumba:/your-stuff/profile',
42
45
  icon: 'globe',
@@ -152,8 +155,6 @@ async function factory (pkgName) {
152
155
  getUserByTokenDur: '1m'
153
156
  }
154
157
  }
155
- this.userGuards = []
156
- this.teamGuards = []
157
158
  this.unsafeUserFields = ['password']
158
159
  this.selfBind(['createNewSite', 'removeSite', 'getSite', 'getUserById', 'getUserByToken', 'getUserByUsernamePassword'])
159
160
  }
@@ -170,10 +171,10 @@ async function factory (pkgName) {
170
171
 
171
172
  start = async () => {
172
173
  const { getModel } = this.app.dobo
173
- if (this.config.interSiteAdmins.length === 0) {
174
+ if (this.config.crossSiteAdmins.length === 0) {
174
175
  const site = await getModel('SumbaSite').findOneRecord({ query: { alias: 'default' } }, { noMagic: true })
175
176
  const user = await getModel('SumbaUser').findOneRecord({ query: { username: 'admin', siteId: site.id } }, { noMagic: true })
176
- this.config.interSiteAdmins.push(user.id)
177
+ this.config.crossSiteAdmins.push(user.id)
177
178
  }
178
179
  }
179
180
 
@@ -228,7 +229,9 @@ async function factory (pkgName) {
228
229
  }, {
229
230
  title: 'permission',
230
231
  children: [
231
- { title: 'routeGuard', href: `waibuAdmin:/${prefix}/route-guard/list` }
232
+ { title: 'routeGuard', href: `waibuAdmin:/${prefix}/route-guard/list` },
233
+ { title: 'modelGuard', href: `waibuAdmin:/${prefix}/model-guard/list` },
234
+ { title: 'attribGuard', href: `waibuAdmin:/${prefix}/attrib-guard/list` }
232
235
  ]
233
236
  }, {
234
237
  title: 'supportSystem',
@@ -272,13 +275,14 @@ async function factory (pkgName) {
272
275
 
273
276
  verifySession = async (req, reply, source, payload) => {
274
277
  const { routePath } = this.app.waibu
278
+ const { query, params } = req
275
279
 
276
280
  if (!req.session) return false
277
281
  if (req.session.userId) {
278
282
  req.user = await this.getUserById(req.session.userId, req)
279
283
  return true
280
284
  }
281
- const redir = routePath(this.config.redirect.signin, req)
285
+ const redir = routePath(this.config.redirect.signin, { query, params })
282
286
  req.session.ref = req.url
283
287
  throw this.error('_redirect', { redirect: redir })
284
288
  }
@@ -361,7 +365,7 @@ async function factory (pkgName) {
361
365
  return true
362
366
  }
363
367
 
364
- checkPathsByRoute = ({ paths = [], method = 'GET', teams = [], guards = [] }) => {
368
+ checkPathsByRoute = ({ paths = [], teamIds = [], guards = [] }) => {
365
369
  const { includes } = this.app.lib.aneka
366
370
  const { outmatch } = this.app.lib
367
371
 
@@ -369,10 +373,8 @@ async function factory (pkgName) {
369
373
  const matchPath = outmatch(item.path)
370
374
  for (const path of paths) {
371
375
  if (matchPath(path)) {
372
- if (item.methods.includes(method)) {
373
- if (teams.length === 0) return item
374
- if (includes(teams, item.teams)) return item
375
- }
376
+ if (includes(teamIds, item.teamIds)) return item
377
+ if (teamIds.length === 0) return item
376
378
  }
377
379
  }
378
380
  }
@@ -535,7 +537,12 @@ async function factory (pkgName) {
535
537
  const { map, orderBy } = this.app.lib._
536
538
  if (!reread) return this.routeGuards
537
539
  const model = getModel('SumbaRouteGuard')
538
- const results = await model.findAllRecord({ query: { status: 'ACTIVE' } }, { noMagic: true, dataOnly: true })
540
+ let results = await model.findAllRecord({ query: { status: 'ACTIVE' } }, { noMagic: true, noCache: true, noDriverHook: true, dataOnly: true })
541
+ results = results.map(item => {
542
+ item.teamIds = item.teamIds ?? []
543
+ item.methods = item.methods ?? []
544
+ return item
545
+ })
539
546
  this.routeGuards = orderBy(map(results, item => {
540
547
  item.path = routePath(item.path)
541
548
  return item
package/lib/get-user.js CHANGED
@@ -3,7 +3,7 @@ import { parseNsSettings } from './util.js'
3
3
  export async function mergeTeam (user) {
4
4
  const { map, pick } = this.app.lib._
5
5
  const { getModel } = this.app.dobo
6
- user.interSiteAdmin = this.config.interSiteAdmins.includes(user.id)
6
+ user.crossSiteAdmin = this.config.crossSiteAdmins.includes(user.id)
7
7
  user.teams = []
8
8
  const query = { userId: user.id, siteId: user.siteId }
9
9
  let mdl = getModel('SumbaTeamUser')
@@ -45,7 +45,7 @@ export async function getUserById (id, req) {
45
45
  user = await getCache({ key })
46
46
  if (user) return JSON.parse(user)
47
47
  }
48
- user = await getModel('SumbaUser').getRecord(id, { noHook: true, throwNotFound: false, req })
48
+ user = await getModel('SumbaUser').getRecord(id, { noHook: true, noDriverHook: true, throwNotFound: false, req })
49
49
  if (!user) return
50
50
  await mergeTeam.call(this, user)
51
51
  if (setCache) {
@@ -63,7 +63,7 @@ export async function getUserByToken (token, req) {
63
63
  user = await getCache({ key })
64
64
  if (user) return JSON.parse(user)
65
65
  }
66
- user = await getModel('SumbaUser').findOneRecord({ query: { token } }, { noHook: true, throwNotFound: false, req })
66
+ user = await getModel('SumbaUser').findOneRecord({ query: { token } }, { noHook: true, noDriverHook: true, throwNotFound: false, req })
67
67
  if (!user) return
68
68
  await mergeTeam.call(this, user)
69
69
  if (setCache) {
@@ -80,7 +80,7 @@ export async function getUserByUsernamePassword (username = '', password = '', r
80
80
  const bcrypt = await importPkg('bajoExtra:bcrypt')
81
81
 
82
82
  const query = { username, provider: 'local', siteId: req.site.id }
83
- const user = await model.findOneRecord({ query }, { req, forceNoHidden: true, noHook: true })
83
+ const user = await model.findOneRecord({ query }, { req, forceNoHidden: true, noHook: true, noDriverHook: true })
84
84
  if (!user) throw this.error('validationError', { details: [{ field: 'username', error: 'Unknown username' }], statusCode: 401 })
85
85
  if (user.status !== 'ACTIVE') throw this.error('validationError', { details: ['User is inactive or temporarily disabled'], statusCode: 401 })
86
86
  const verified = await bcrypt.compare(password, user.password)
package/lib/util.js CHANGED
@@ -56,17 +56,21 @@ export async function checkTheme (req, reply) {
56
56
  req.theme = req.theme ?? 'default'
57
57
  }
58
58
 
59
- export async function checkTeam (req, reply, source) {
60
- if (!req.user) return
59
+ export async function checkTeam (req, reply, route) {
61
60
  const { map } = this.app.lib._
62
- const paths = pathsToCheck.call(this, req, true)
61
+ route.teams = route.teams ?? []
62
+ if (route.teams.length === 0) return
63
+
63
64
  const teams = map(req.user.teams, 'alias')
65
+ if (teams.includes('administrator')) return
66
+ if (teams.length === 0) throw this.error('accessDenied', { statusCode: 403 })
67
+
68
+ const paths = pathsToCheck.call(this, req, true)
64
69
  const allGuards = (await this.getRouteGuards()).filter(item => item.siteId === req.site.id + '' && item.teams.length > 0)
65
- if (allGuards.length === 0) return // no route guarded
66
- let match = this.checkPathsByRoute({ paths, method: req.method, teams, guards: allGuards.filter(item => !item.inverse) })
67
- if (!match) return // route is NOT protected by team guard
70
+ if (allGuards.length === 0) return // no route to be team guarded
71
+ let match = this.checkPathsByRoute({ paths, teams, guards: allGuards.filter(item => !item.inverse) })
68
72
  if (match) {
69
- const neg = this.checkPathsByRoute({ paths, method: req.method, teams, guards: allGuards.filter(item => item.inverse) })
73
+ const neg = this.checkPathsByRoute({ paths, teams, guards: allGuards.filter(item => item.inverse) })
70
74
  if (neg) match = undefined
71
75
  }
72
76
  if (!match) throw this.error('accessDenied', { statusCode: 403 })
@@ -102,14 +106,14 @@ export async function checkUserId (req, reply, source) {
102
106
  const paths = pathsToCheck.call(this, req)
103
107
  const allGuards = (await this.getRouteGuards()).filter(item => item.siteId === req.site.id + '')
104
108
  if (allGuards.length === 0) return false // no routes protected
105
- let securePath = await this.checkPathsByRoute({ paths, method: req.method, guards: allGuards.filter(item => !item.anonymous && !item.inverse) })
109
+ let securePath = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => !item.anonymous && !item.inverse) })
106
110
  if (securePath) {
107
- const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: allGuards.filter(item => !item.anonymous && item.inverse) })
111
+ const neg = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => !item.anonymous && item.inverse) })
108
112
  if (neg) securePath = undefined
109
113
  }
110
- let anonymousPath = await this.checkPathsByRoute({ paths, method: req.method, guards: allGuards.filter(item => item.anonymous && !item.inverse) })
114
+ let anonymousPath = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => item.anonymous && !item.inverse) })
111
115
  if (anonymousPath) {
112
- const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: allGuards.filter(item => item.anonymous && item.inverse) })
116
+ const neg = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => item.anonymous && item.inverse) })
113
117
  if (neg) anonymousPath = undefined
114
118
  }
115
119
  if (!securePath && !anonymousPath) {
@@ -121,9 +125,10 @@ export async function checkUserId (req, reply, source) {
121
125
  req.session.ref = req.url
122
126
  return reply.redirectTo(routePath(this.config.redirect.signout))
123
127
  }
128
+ if (!(securePath.methods ?? []).includes(req.method)) throw this.error('accessDenied', { statusCode: 403 })
124
129
  if (userId) {
125
130
  await setUser()
126
- return true
131
+ return securePath
127
132
  }
128
133
  const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
129
134
  const payload = silentOnError ? { noContent: true } : undefined
@@ -140,7 +145,7 @@ export async function checkUserId (req, reply, source) {
140
145
  }
141
146
  }
142
147
  if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
143
- return true
148
+ return securePath
144
149
  }
145
150
 
146
151
  export async function latLngHook (body, options) {
@@ -157,10 +162,8 @@ export async function latLngHook (body, options) {
157
162
  * @param {Object} reply - Reply object
158
163
  * @returns
159
164
  */
160
- export async function checkInterSite (req, reply) {
165
+ export async function checkCrossSite (req, reply) {
161
166
  const { get } = this.app.lib._
162
- const isinterSite = get(req, 'routeOptions.config.interSite')
163
- const isInterSiteAdmin = get(req, 'user.interSiteAdmin')
164
- if (!isinterSite) return
165
- if (!isInterSiteAdmin) throw this.error('accessDenied', { statusCode: 403 })
167
+ if (!get(req, 'routeOptions.config.crossSite')) return
168
+ if (!get(req, 'user.crossSiteAdmin')) throw this.error('accessDenied', { statusCode: 403 })
166
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.23.0",
3
+ "version": "2.24.0",
4
4
  "description": "Biz Suite for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## 2026-05-11
4
4
 
5
+ - [2.24.0] Complete rewrite of route guards
6
+ - [2.24.0] Add model guards
7
+ - [2.24.0] Add attribute guards
8
+
9
+ ## 2026-05-11
10
+
5
11
  - [2.23.0] Unify route guards and put it in ```SumbaRouteGuard``` database
6
12
  - [2.23.0] Phased out ```setting.noRoutes```
7
13
 
@@ -1,6 +0,0 @@
1
- async function afterAction (action, ...args) {
2
- if (!['createRecord', 'updateRecord', 'removeRecord'].includes(action)) return
3
- await this.getTeamGuards(true)
4
- }
5
-
6
- export default afterAction
@@ -1,6 +0,0 @@
1
- async function afterAction (action, ...args) {
2
- if (!['createRecord', 'updateRecord', 'removeRecord'].includes(action)) return
3
- await this.getUserGuards(true)
4
- }
5
-
6
- export default afterAction
@@ -1,17 +0,0 @@
1
- const doboBeforeCreateRecord = {
2
- level: 1000,
3
- handler: async function (modelName, body, options = {}) {
4
- const { get } = this.app.lib._
5
- const { req } = options
6
- if (options.noAutoFilter || !req || get(req, 'routeOptions.config.interSite')) return
7
- const item = { siteId: 'site.id', userId: 'user.id' }
8
- const model = this.app.dobo.getModel(modelName)
9
- for (const i in item) {
10
- const rec = get(req, item[i])
11
- const field = model.getProperty(i)
12
- if (rec && field) body[i] = field.type === 'string' ? (rec + '') : rec
13
- }
14
- }
15
- }
16
-
17
- export default doboBeforeCreateRecord
@@ -1,63 +0,0 @@
1
- const useAdmin = ['waibuAdmin']
2
-
3
- export async function rebuildFilter (modelName, filter, req) {
4
- const { isEmpty, isPlainObject, map, find, get } = this.app.lib._
5
- filter.query = filter.query ?? {}
6
- if (req.routeOptions.config.interSite) {
7
- filter.query = { $and: [filter.query] }
8
- return
9
- }
10
-
11
- const queryBySiteSetting = (query) => {
12
- if (!req.site) return
13
- const setting = get(req, `site.setting.dobo.query.${modelName}`)
14
- if (isPlainObject(setting) && !isEmpty(setting)) query.$and.push(setting)
15
- }
16
-
17
- const queryByTeamSetting = (query) => {
18
- if (!req.user) return
19
- const q = []
20
- for (const team of req.user.teams) {
21
- const item = get(team, `setting.dobo.query.${modelName}`)
22
- if (item) q.push(item)
23
- }
24
- if (isEmpty(q)) return
25
- if (q.length === 1) query.$and.push(q[0])
26
- else query.$and.push({ $or: q })
27
- }
28
-
29
- const model = this.app.dobo.getModel(modelName)
30
- const hasSiteId = model.hasProperty('siteId')
31
- const hasUserId = model.hasProperty('userId')
32
- const hasTeamId = model.hasProperty('teamId')
33
- const isAdmin = find(get(req, 'user.teams', []), { alias: 'administrator' }) && useAdmin.includes(get(req, 'routeOptions.config.ns'))
34
- const q = { $and: [] }
35
- queryBySiteSetting(q)
36
- queryByTeamSetting(q)
37
- if (!isEmpty(filter.query)) {
38
- if (filter.query.$and) q.$and.push(...filter.query.$and)
39
- else q.$and.push(filter.query)
40
- }
41
- if (hasSiteId) q.$and.push({ siteId: req.site.id })
42
- if (hasTeamId && !isAdmin) {
43
- const teamIds = map(req.user.teams, 'id')
44
- if (hasUserId) q.$and.push({ $or: [{ teamId: { $in: teamIds } }, { userId: req.user.id }] })
45
- else q.$and.push({ teamId: { $in: teamIds } })
46
- } else if (!isAdmin) {
47
- if (hasUserId) q.$and.push({ userId: req.user.id })
48
- }
49
- filter.query = q
50
- }
51
-
52
- export async function handler (modelName, filter, options = {}) {
53
- const { req } = options
54
- if (options.noAutoFilter || !req) return
55
- await rebuildFilter.call(this, modelName, filter, req)
56
- }
57
-
58
- const doboBeforeFindRecord = {
59
- level: 1000,
60
- handler
61
- }
62
-
63
- export default doboBeforeFindRecord
@@ -1,23 +0,0 @@
1
- import { rebuildFilter } from './dobo@before-find-record.js'
2
-
3
- export async function checker (modelName, id, options = {}) {
4
- const { req } = options
5
-
6
- const model = this.app.dobo.getModel(modelName)
7
- if (options.noAutoFilter || !req) return
8
- const filter = {}
9
- await rebuildFilter.call(this, modelName, filter, req)
10
- if (filter.query.$and) filter.query.$and.push({ id })
11
- else filter.query.id = id
12
- const row = await model.findOneRecord(filter, { count: false })
13
- if (!row) throw this.app.dobo.error('recordNotFound%s%s', id, this.name, { statusCode: 404 })
14
- }
15
-
16
- const doboBeforeGetRecord = {
17
- level: 1000,
18
- handler: async function (modelName, id, options) {
19
- await checker.call(this, modelName, id, options)
20
- }
21
- }
22
-
23
- export default doboBeforeGetRecord
@@ -1,10 +0,0 @@
1
- import { checker } from './dobo@before-get-record.js'
2
-
3
- const doboBeforeRemoveRecord = {
4
- level: 1000,
5
- handler: async function (modelName, id, options = {}) {
6
- await checker.call(this, modelName, id, options.req)
7
- }
8
- }
9
-
10
- export default doboBeforeRemoveRecord
@@ -1,10 +0,0 @@
1
- import { checker } from './dobo@before-get-record.js'
2
-
3
- const doboBeforeUpdateRecord = {
4
- level: 1000,
5
- handler: async function (modelName, id, body, options = {}) {
6
- await checker.call(this, modelName, id, options)
7
- }
8
- }
9
-
10
- export default doboBeforeUpdateRecord