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.
- package/extend/bajo/hook/bajo.extend@after-read-config.js +13 -0
- package/extend/bajo/hook/dobo.sumba-route-guard@after-transaction.js +6 -0
- package/extend/bajo/hook/dobo@before-count-record.js +3 -3
- package/extend/bajo/hook/dobo@before-driver-create-record.js +17 -0
- package/extend/bajo/hook/dobo@before-driver-find-all-record.js +13 -0
- package/extend/bajo/hook/dobo@before-driver-find-record.js +96 -0
- package/extend/bajo/hook/dobo@before-driver-get-record.js +22 -0
- package/extend/bajo/hook/dobo@before-driver-remove-record.js +10 -0
- package/extend/bajo/hook/dobo@before-driver-update-record.js +10 -0
- package/extend/bajo/hook/waibu-mpa@pre-parsing.js +5 -4
- package/extend/bajo/hook/waibu-rest-api@pre-parsing.js +5 -4
- package/extend/bajo/hook/waibu-static@pre-parsing.js +5 -4
- package/extend/bajo/hook/waibu@after-app-boot.js +5 -23
- package/extend/bajo/hook/waibu@pre-parsing.js +0 -5
- package/extend/bajo/intl/en-US.json +13 -6
- package/extend/bajo/intl/id.json +14 -2
- package/extend/dobo/feature/team-ids.js +20 -0
- package/extend/dobo/fixture/route-guard.js +26 -0
- package/extend/dobo/model/attrib-guard.js +36 -0
- package/extend/dobo/model/model-guard.js +37 -0
- package/extend/dobo/model/route-guard.js +45 -0
- package/extend/waibuDb/schema/attrib-guard.js +15 -0
- package/extend/waibuDb/schema/model-guard.js +15 -0
- package/extend/waibuDb/schema/route-guard.js +15 -0
- package/extend/waibuDb/schema/user.js +19 -26
- package/extend/waibuMpa/extend/waibuAdmin/route/all-sites/@action.js +1 -1
- package/extend/waibuMpa/extend/waibuAdmin/route/attrib-guard/@action.js +11 -0
- package/extend/waibuMpa/extend/waibuAdmin/route/cache/@action.js +1 -1
- package/extend/waibuMpa/extend/waibuAdmin/route/model-guard/@action.js +11 -0
- package/extend/waibuMpa/extend/waibuAdmin/route/reset-user-password.js +2 -1
- package/extend/waibuMpa/extend/waibuAdmin/route/route-guard/@action.js +11 -0
- package/extend/waibuMpa/extend/waibuAdmin/route/session/@action.js +1 -1
- package/extend/waibuRestApi/route/manage/route-guard/model-builder.json +4 -0
- package/index.js +37 -47
- package/lib/get-user.js +4 -4
- package/lib/util.js +53 -74
- package/package.json +1 -1
- package/wiki/CHANGES.md +11 -0
- package/extend/bajo/hook/dobo@before-create-record.js +0 -17
- package/extend/bajo/hook/dobo@before-find-record.js +0 -63
- package/extend/bajo/hook/dobo@before-get-record.js +0 -23
- package/extend/bajo/hook/dobo@before-remove-record.js +0 -10
- package/extend/bajo/hook/dobo@before-update-record.js +0 -10
- package/extend/sumba/route-guard/anonymous.json +0 -10
- package/extend/sumba/route-guard/secure.json +0 -8
- 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'
|
|
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'
|
|
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
|
}
|
|
@@ -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
|
|
@@ -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
|
|
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
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
if (
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
61
|
+
route.teams = route.teams ?? []
|
|
62
|
+
if (route.teams.length === 0) return
|
|
63
|
+
|
|
91
64
|
const teams = map(req.user.teams, 'alias')
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
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
|