sumba 2.22.1 → 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 (46) 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/hook/waibu@after-app-boot.js +5 -23
  14. package/extend/bajo/hook/waibu@pre-parsing.js +0 -5
  15. package/extend/bajo/intl/en-US.json +13 -6
  16. package/extend/bajo/intl/id.json +14 -2
  17. package/extend/dobo/feature/team-ids.js +20 -0
  18. package/extend/dobo/fixture/route-guard.js +26 -0
  19. package/extend/dobo/model/attrib-guard.js +36 -0
  20. package/extend/dobo/model/model-guard.js +37 -0
  21. package/extend/dobo/model/route-guard.js +45 -0
  22. package/extend/waibuDb/schema/attrib-guard.js +15 -0
  23. package/extend/waibuDb/schema/model-guard.js +15 -0
  24. package/extend/waibuDb/schema/route-guard.js +15 -0
  25. package/extend/waibuDb/schema/user.js +19 -26
  26. package/extend/waibuMpa/extend/waibuAdmin/route/all-sites/@action.js +1 -1
  27. package/extend/waibuMpa/extend/waibuAdmin/route/attrib-guard/@action.js +11 -0
  28. package/extend/waibuMpa/extend/waibuAdmin/route/cache/@action.js +1 -1
  29. package/extend/waibuMpa/extend/waibuAdmin/route/model-guard/@action.js +11 -0
  30. package/extend/waibuMpa/extend/waibuAdmin/route/reset-user-password.js +2 -1
  31. package/extend/waibuMpa/extend/waibuAdmin/route/route-guard/@action.js +11 -0
  32. package/extend/waibuMpa/extend/waibuAdmin/route/session/@action.js +1 -1
  33. package/extend/waibuRestApi/route/manage/route-guard/model-builder.json +4 -0
  34. package/index.js +37 -47
  35. package/lib/get-user.js +4 -4
  36. package/lib/util.js +53 -74
  37. package/package.json +1 -1
  38. package/wiki/CHANGES.md +11 -0
  39. package/extend/bajo/hook/dobo@before-create-record.js +0 -17
  40. package/extend/bajo/hook/dobo@before-find-record.js +0 -63
  41. package/extend/bajo/hook/dobo@before-get-record.js +0 -23
  42. package/extend/bajo/hook/dobo@before-remove-record.js +0 -10
  43. package/extend/bajo/hook/dobo@before-update-record.js +0 -10
  44. package/extend/sumba/route-guard/anonymous.json +0 -10
  45. package/extend/sumba/route-guard/secure.json +0 -8
  46. package/lib/collect.js +0 -52
@@ -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,
@@ -0,0 +1,11 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'routeGuard',
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, 'SumbaRouteGuard', 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: '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')
@@ -0,0 +1,4 @@
1
+ {
2
+ "model": "SumbaRouteGuard",
3
+ "disabled": ["create", "update", "remove"]
4
+ }
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',
@@ -160,10 +163,6 @@ async function factory (pkgName) {
160
163
  const { getPluginDataDir } = this.app.bajo
161
164
  this.downloadDir = `${getPluginDataDir(this.ns)}/download`
162
165
  this.app.lib.fs.ensureDirSync(this.downloadDir)
163
- for (const type of ['secure', 'anonymous', 'team']) {
164
- this[`${type}Routes`] = this[`${type}Routes`] ?? []
165
- this[`${type}NegRoutes`] = this[`${type}NegRoutes`] ?? []
166
- }
167
166
  if (this.config.multiSite === true) {
168
167
  this.config.multiSite = cloneDeep(defMultiSite)
169
168
  this.config.multiSite.enabled = true
@@ -172,10 +171,10 @@ async function factory (pkgName) {
172
171
 
173
172
  start = async () => {
174
173
  const { getModel } = this.app.dobo
175
- if (this.config.interSiteAdmins.length === 0) {
174
+ if (this.config.crossSiteAdmins.length === 0) {
176
175
  const site = await getModel('SumbaSite').findOneRecord({ query: { alias: 'default' } }, { noMagic: true })
177
176
  const user = await getModel('SumbaUser').findOneRecord({ query: { username: 'admin', siteId: site.id } }, { noMagic: true })
178
- this.config.interSiteAdmins.push(user.id)
177
+ this.config.crossSiteAdmins.push(user.id)
179
178
  }
180
179
  }
181
180
 
@@ -227,6 +226,13 @@ async function factory (pkgName) {
227
226
  { title: 'teamUser', href: `waibuAdmin:/${prefix}/team-user/list` },
228
227
  { title: 'teamSetting', href: `waibuAdmin:/${prefix}/team-setting/list` }
229
228
  ]
229
+ }, {
230
+ title: 'permission',
231
+ children: [
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` }
235
+ ]
230
236
  }, {
231
237
  title: 'supportSystem',
232
238
  children: [
@@ -269,13 +275,14 @@ async function factory (pkgName) {
269
275
 
270
276
  verifySession = async (req, reply, source, payload) => {
271
277
  const { routePath } = this.app.waibu
278
+ const { query, params } = req
272
279
 
273
280
  if (!req.session) return false
274
281
  if (req.session.userId) {
275
282
  req.user = await this.getUserById(req.session.userId, req)
276
283
  return true
277
284
  }
278
- const redir = routePath(this.config.redirect.signin, req)
285
+ const redir = routePath(this.config.redirect.signin, { query, params })
279
286
  req.session.ref = req.url
280
287
  throw this.error('_redirect', { redirect: redir })
281
288
  }
@@ -358,7 +365,7 @@ async function factory (pkgName) {
358
365
  return true
359
366
  }
360
367
 
361
- checkPathsByTeam = ({ paths = [], method = 'GET', teams = [], guards = [] }) => {
368
+ checkPathsByRoute = ({ paths = [], teamIds = [], guards = [] }) => {
362
369
  const { includes } = this.app.lib.aneka
363
370
  const { outmatch } = this.app.lib
364
371
 
@@ -366,24 +373,8 @@ async function factory (pkgName) {
366
373
  const matchPath = outmatch(item.path)
367
374
  for (const path of paths) {
368
375
  if (matchPath(path)) {
369
- const matchMethods = outmatch(item.methods, { separator: false })
370
- if (matchMethods(method)) {
371
- if (item.teams.length === 0) return item
372
- if (includes(teams, item.teams)) return item
373
- }
374
- }
375
- }
376
- }
377
- }
378
-
379
- checkPathsByRoute = ({ paths = [], method = 'GET', guards = [] }) => {
380
- const { outmatch } = this.app.lib
381
- for (const item of guards) {
382
- const matchPath = outmatch(item.path)
383
- for (const path of paths) {
384
- if (matchPath(path)) {
385
- const matchMethods = outmatch(item.methods, { separator: false })
386
- if (matchMethods(method)) return item
376
+ if (includes(teamIds, item.teamIds)) return item
377
+ if (teamIds.length === 0) return item
387
378
  }
388
379
  }
389
380
  }
@@ -535,31 +526,30 @@ async function factory (pkgName) {
535
526
  return password
536
527
  }
537
528
 
538
- parseRouteGuard = item => {
539
- const { routePath, routePathHandlers } = this.app.waibu
540
- const { isString, isEmpty } = this.app.lib._
541
- if (isString(item)) {
542
- let [path, methods] = item.split('|').map(i => i.trim())
543
- methods = isEmpty(methods) ? ['*'] : methods.split(',').map(i => i.trim())
544
- item = { path, methods }
545
- }
546
- item.methods = item.methods ?? ['*']
547
- if (item.methods.includes('*')) item.methods = ['*']
548
- const [, routeHandler] = item.path.split(':')[0].split('.')
549
- if (!isEmpty(routeHandler) && !routePathHandlers[routeHandler]) return
550
- const rns = isEmpty(routeHandler) ? 'waibuMpa' : routePathHandlers[routeHandler].ns
551
- if (!this.app[rns]) return
552
- item.inverse = item.path[0] === '!'
553
- if (item.inverse) item.path = item.path.slice(1)
554
- item.path = routePath(item.path, { defFormat: false })
555
- return item
556
- }
557
-
558
529
  hash = async (item) => {
559
530
  const { hash } = this.app.bajoExtra
560
531
  return await hash(item, this.config.auth.common.apiKey.algo)
561
532
  }
562
533
 
534
+ getRouteGuards = async (reread) => {
535
+ const { getModel } = this.app.dobo
536
+ const { routePath } = this.app.waibu
537
+ const { map, orderBy } = this.app.lib._
538
+ if (!reread) return this.routeGuards
539
+ const model = getModel('SumbaRouteGuard')
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
+ })
546
+ this.routeGuards = orderBy(map(results, item => {
547
+ item.path = routePath(item.path)
548
+ return item
549
+ }), ['weight', 'path'])
550
+ return this.routeGuards
551
+ }
552
+
563
553
  createNewSite = createNewSite
564
554
  removeSite = removeSite
565
555
  getSite = getSite
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
@@ -1,9 +1,6 @@
1
- export const checkNoRouteSettingKey = 'setting.waibu.noRoutes.$in'
2
-
3
1
  export function parseNsSettings (ns, setting, items) {
4
- const { trim, set, isPlainObject, isArray, isEmpty, find, get, isString } = this.app.lib._
2
+ const { trim, set, isPlainObject, isArray, isEmpty, find } = this.app.lib._
5
3
  const { parseObject, dayjs } = this.app.lib
6
- const { routePath } = this.app.waibu
7
4
 
8
5
  for (const item of items) {
9
6
  if (item.ns === '_var' || ns === '_var') continue
@@ -27,30 +24,6 @@ export function parseNsSettings (ns, setting, items) {
27
24
  if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
28
25
  set(setting, `${ns}.${item.key}`, value)
29
26
  }
30
- const key = checkNoRouteSettingKey.slice(checkNoRouteSettingKey.indexOf('.') + 1)
31
- let noRoutes = get(setting, key, [])
32
- if (noRoutes.length > 0) {
33
- noRoutes = noRoutes.map(item => {
34
- if (!isString(item)) return item
35
- let [url, methods] = item.split('|')
36
- if (methods === '*' || !methods) methods = 'GET,POST,PUT,DELETE'
37
- methods = methods.split(',').map(m => m.trim().toUpperCase())
38
- return { url: routePath(url), methods }
39
- })
40
- set(setting, key, noRoutes)
41
- }
42
- }
43
-
44
- export function checkNoRouteSetting (req, routes) {
45
- const { isEmpty } = this.app.lib._
46
- const { outmatch } = this.app.lib
47
- if (isEmpty(routes)) return
48
- for (const route of routes) {
49
- const isMatchUrl = outmatch(route.url)
50
- if (isMatchUrl(req.url) || isMatchUrl(req.routeOptions.url)) {
51
- if (route.methods.includes(req.method)) throw this.error('accessDenied', { statusCode: 403 })
52
- }
53
- }
54
27
  }
55
28
 
56
29
  export function pathsToCheck (req, withHome) {
@@ -83,26 +56,25 @@ export async function checkTheme (req, reply) {
83
56
  req.theme = req.theme ?? 'default'
84
57
  }
85
58
 
86
- export async function checkTeam (req, reply, source) {
87
- const { get, isEmpty } = this.app.lib._
88
- if (!req.user) return
59
+ export async function checkTeam (req, reply, route) {
89
60
  const { map } = this.app.lib._
90
- const paths = pathsToCheck.call(this, req, true)
61
+ route.teams = route.teams ?? []
62
+ if (route.teams.length === 0) return
63
+
91
64
  const teams = map(req.user.teams, 'alias')
92
- let match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamRoutes })
93
- if (!match) match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamNegRoutes })
94
- if (!match) {
95
- const guards = map(this.teamRoutes, 'path')
96
- match = !this.checkPathsByGuard({ paths, guards })
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)
69
+ const allGuards = (await this.getRouteGuards()).filter(item => item.siteId === req.site.id + '' && item.teams.length > 0)
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) })
72
+ if (match) {
73
+ const neg = this.checkPathsByRoute({ paths, teams, guards: allGuards.filter(item => item.inverse) })
74
+ if (neg) match = undefined
97
75
  }
98
76
  if (!match) throw this.error('accessDenied', { statusCode: 403 })
99
- const routes = []
100
- for (const team of (req.user.teams ?? [])) {
101
- const items = get(team, checkNoRouteSettingKey, [])
102
- if (isEmpty(items)) continue
103
- routes.push(...items)
104
- }
105
- checkNoRouteSetting.call(this, req, routes)
77
+ // passed
106
78
  }
107
79
 
108
80
  export async function checkUserId (req, reply, source) {
@@ -132,46 +104,48 @@ export async function checkUserId (req, reply, source) {
132
104
  }
133
105
 
134
106
  const paths = pathsToCheck.call(this, req)
135
- let securePath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureRoutes })
107
+ const allGuards = (await this.getRouteGuards()).filter(item => item.siteId === req.site.id + '')
108
+ if (allGuards.length === 0) return false // no routes protected
109
+ let securePath = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => !item.anonymous && !item.inverse) })
136
110
  if (securePath) {
137
- const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureNegRoutes })
111
+ const neg = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => !item.anonymous && item.inverse) })
138
112
  if (neg) securePath = undefined
139
113
  }
140
- let anonymousPath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousRoutes })
114
+ let anonymousPath = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => item.anonymous && !item.inverse) })
141
115
  if (anonymousPath) {
142
- const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousNegRoutes })
116
+ const neg = await this.checkPathsByRoute({ paths, guards: allGuards.filter(item => item.anonymous && item.inverse) })
143
117
  if (neg) anonymousPath = undefined
144
118
  }
145
119
  if (!securePath && !anonymousPath) {
146
120
  if (userId) await setUser()
147
- return
121
+ return false // regular, unguarded path. Not secure & not anonymous path
148
122
  }
149
123
  if (anonymousPath) {
150
- if (!userId) return
124
+ if (!userId) return false
151
125
  req.session.ref = req.url
152
126
  return reply.redirectTo(routePath(this.config.redirect.signout))
153
127
  }
154
- if (securePath) {
155
- if (userId) {
156
- await setUser()
157
- return
158
- }
159
- const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
160
- const payload = silentOnError ? { noContent: true } : undefined
161
- const authMethods = this.config.auth[webApp].methods ?? []
162
- if (isEmpty(authMethods)) throw this.error('noAuthMethod', merge({ statusCode: 500 }, payload))
163
- let success
164
- for (const m of authMethods) {
165
- const handler = this[camelCase(`verify ${m}`)]
166
- if (!handler) throw this.error('invalidAuthMethod%s', m, merge({ statusCode: 500 }, payload))
167
- const check = await handler(req, reply, source, payload)
168
- if (check) {
169
- success = check
170
- break
171
- }
128
+ if (!(securePath.methods ?? []).includes(req.method)) throw this.error('accessDenied', { statusCode: 403 })
129
+ if (userId) {
130
+ await setUser()
131
+ return securePath
132
+ }
133
+ const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
134
+ const payload = silentOnError ? { noContent: true } : undefined
135
+ const authMethods = this.config.auth[webApp].methods ?? []
136
+ if (isEmpty(authMethods)) throw this.error('noAuthMethod', merge({ statusCode: 500 }, payload))
137
+ let success
138
+ for (const m of authMethods) {
139
+ const handler = this[camelCase(`verify ${m}`)]
140
+ if (!handler) throw this.error('invalidAuthMethod%s', m, merge({ statusCode: 500 }, payload))
141
+ const check = await handler(req, reply, source, payload)
142
+ if (check) {
143
+ success = check
144
+ break
172
145
  }
173
- if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
174
146
  }
147
+ if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
148
+ return securePath
175
149
  }
176
150
 
177
151
  export async function latLngHook (body, options) {
@@ -181,10 +155,15 @@ export async function latLngHook (body, options) {
181
155
  body[options.field] = round(body[options.field], options.scale)
182
156
  }
183
157
 
184
- export async function checkinterSite (req, reply) {
158
+ /**
159
+ * If current route is an inter site route user is an inter site admin, then let it passed
160
+ *
161
+ * @param {Object} req - Request object
162
+ * @param {Object} reply - Reply object
163
+ * @returns
164
+ */
165
+ export async function checkCrossSite (req, reply) {
185
166
  const { get } = this.app.lib._
186
- const isinterSite = get(req, 'routeOptions.config.interSite')
187
- const isInterSiteAdmin = get(req, 'user.interSiteAdmin')
188
- if (!isinterSite) return
189
- 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 })
190
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.22.1",
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
@@ -1,5 +1,16 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-11
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
+
11
+ - [2.23.0] Unify route guards and put it in ```SumbaRouteGuard``` database
12
+ - [2.23.0] Phased out ```setting.noRoutes```
13
+
3
14
  ## 2026-04-25
4
15
 
5
16
  - [2.21.1] Bug fix on team routes collections
@@ -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