sumba 2.30.0 → 2.32.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.js +39 -30
- package/extend/bajo/intl/en-US.json +3 -1
- package/extend/bajo/intl/id.json +3 -1
- package/extend/dobo/feature/lat-lng.js +6 -1
- package/extend/dobo/feature/lat.js +1 -1
- package/extend/dobo/feature/lng.js +1 -1
- package/extend/dobo/model.js +2 -2
- package/extend/dobo/model.json +1 -0
- package/extend/waibuDb/schema/team-user.js +2 -2
- package/index.js +167 -12
- package/lib/create-new-site.js +2 -2
- package/lib/get-site.js +1 -3
- package/lib/get-user.js +2 -6
- package/package.json +1 -1
- package/wiki/CHANGES.md +13 -0
- package/lib/util.js +0 -162
package/extend/bajo/hook.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { checkUserId, checkTeam, checkXSite, checkTheme, checkIconset } from '../../lib/util.js'
|
|
2
1
|
import { removeRefs } from '../../lib/remove-site.js'
|
|
3
2
|
import { getAllFixtures, createRefs } from '../../lib/create-new-site.js'
|
|
4
3
|
import path from 'path'
|
|
@@ -22,8 +21,9 @@ async function clearCacheUser (id, result) {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
async function applyModelGuard ({ model, q, teamIds, options }) {
|
|
25
|
-
const {
|
|
24
|
+
const { set, orderBy } = this.app.lib._
|
|
26
25
|
const { includes } = this.app.lib.aneka
|
|
26
|
+
const { sanitizeByType } = this.app.dobo
|
|
27
27
|
const { req } = options
|
|
28
28
|
const results = []
|
|
29
29
|
|
|
@@ -32,24 +32,29 @@ async function applyModelGuard ({ model, q, teamIds, options }) {
|
|
|
32
32
|
const filterFn = item => {
|
|
33
33
|
const bySiteId = item.siteId === req.site.id + ''
|
|
34
34
|
const byModel = item.models.includes(model.name)
|
|
35
|
-
const byTeamId = includes(item.teamIds, teamIds)
|
|
36
35
|
const byFields = fields.includes(item.field)
|
|
37
|
-
return bySiteId && byModel &&
|
|
36
|
+
return bySiteId && byModel && byFields
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
const rules = orderBy(guards.filter(filterFn), ['field'])
|
|
41
40
|
for (const field of fields) {
|
|
42
|
-
|
|
41
|
+
if (!model.getNonVirtualProperties(true).includes(field)) continue // or, should it throws exception instead?
|
|
42
|
+
const prop = model.getProperty(field)
|
|
43
43
|
const items = rules.filter(item => item.field === field)
|
|
44
44
|
for (const item of items) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const values = item.value.map(val => {
|
|
46
|
+
return sanitizeByType(val, prop.type, { strict: true, inputFormat: 'string', model: model.name })
|
|
47
|
+
})
|
|
48
|
+
const op = item.condition.toLowerCase()
|
|
49
|
+
let value
|
|
50
|
+
if (['in', 'nin'].includes(op)) value = set({}, '$' + op, values)
|
|
51
|
+
else if (op === 'between') value = { $gte: values[0], $lte: values[1] }
|
|
52
|
+
else value = set({}, '$' + op, values[0])
|
|
53
|
+
const teamOk = item.allTeams ? true : (item.teamIds.length === 0 ? false : includes(item.teamIds, teamIds))
|
|
54
|
+
if (!teamOk) throw this.error('_abortAction')
|
|
55
|
+
results.push(set({}, field, value))
|
|
47
56
|
}
|
|
48
|
-
if (values.length) results.push(set({}, field, { $in: values }))
|
|
49
57
|
}
|
|
50
|
-
|
|
51
|
-
const allowEmpty = get(this, `config.dobo.model.${model.name}.allowEmptyQuery`, true)
|
|
52
|
-
if (results.length === 0 && !allowEmpty) throw this.error('_emptyColumnQuery') // signal driver to about with no result immediately
|
|
53
58
|
q.$and.push(...results)
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -63,20 +68,19 @@ export async function applyAttribGuard ({ model, teamIds, options }) {
|
|
|
63
68
|
const filterFn = item => {
|
|
64
69
|
const bySiteId = item.siteId === req.site.id + ''
|
|
65
70
|
const byModel = item.models.includes(model.name)
|
|
66
|
-
const byTeamId = includes(item.teamIds, teamIds)
|
|
71
|
+
const byTeamId = item.allTeams ? true : includes(item.teamIds, teamIds)
|
|
67
72
|
return bySiteId && byModel && byTeamId
|
|
68
73
|
}
|
|
69
|
-
|
|
70
|
-
const item = guards.filter(filterFn)[0]
|
|
71
|
-
if (item) results.push(...item.hiddenFields)
|
|
74
|
+
guards.filter(filterFn).forEach(item => results.push(...item.hiddenFields))
|
|
72
75
|
options.hidden = options.hidden ?? []
|
|
73
|
-
|
|
76
|
+
options.hidden.push(...results)
|
|
74
77
|
options.hidden = uniq(options.hidden)
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
async function rebuildFilter (model, filter = {}, options = {}) {
|
|
78
81
|
const { isEmpty, get } = this.app.lib._
|
|
79
82
|
const { req } = options
|
|
83
|
+
const allowEmpty = !get(this, `app.${model.ns}.config.sumba.allowEmptyQuery`, []).includes(model.name)
|
|
80
84
|
const hasSiteId = model.hasProperty('siteId')
|
|
81
85
|
const hasUserId = model.hasProperty('userId')
|
|
82
86
|
const hasTeamId = model.hasProperty('teamId')
|
|
@@ -90,13 +94,17 @@ async function rebuildFilter (model, filter = {}, options = {}) {
|
|
|
90
94
|
if (filter.query.$and) q.$and.push(...filter.query.$and)
|
|
91
95
|
else q.$and.push(filter.query)
|
|
92
96
|
}
|
|
93
|
-
if (req.routeOptions.config.xSite)
|
|
97
|
+
if (req.routeOptions.config.xSite) {
|
|
98
|
+
filter.query = q
|
|
99
|
+
return
|
|
100
|
+
}
|
|
94
101
|
if (hasSiteId) q.$and.push({ siteId: req.site.id + '' })
|
|
95
102
|
if (aliases.includes('administrator')) {
|
|
96
103
|
filter.query = q
|
|
97
104
|
return
|
|
98
105
|
}
|
|
99
|
-
if (
|
|
106
|
+
if (isEmpty(req.user)) {
|
|
107
|
+
if (q.$and.length === 0 && !allowEmpty) throw this.error('_emptyColumnQuery')
|
|
100
108
|
filter.query = q
|
|
101
109
|
return
|
|
102
110
|
}
|
|
@@ -120,8 +128,9 @@ async function rebuildFilter (model, filter = {}, options = {}) {
|
|
|
120
128
|
else q.$and.push(condTeamId)
|
|
121
129
|
} else if (hasUserId) q.$and.push(condUserId)
|
|
122
130
|
|
|
123
|
-
await applyModelGuard.call(this, { model, q, teamIds, options })
|
|
131
|
+
await applyModelGuard.call(this, { model, q, teamIds, options, allowEmpty })
|
|
124
132
|
await applyAttribGuard.call(this, { model, teamIds, options })
|
|
133
|
+
if (q.$and.length === 0 && !allowEmpty) throw this.error('_emptyColumnQuery')
|
|
125
134
|
filter.query = q
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -338,30 +347,30 @@ async function hook () {
|
|
|
338
347
|
level: 10,
|
|
339
348
|
name: 'waibuMpa:preParsing',
|
|
340
349
|
handler: async function (req, reply) {
|
|
341
|
-
await checkTheme
|
|
342
|
-
await checkIconset
|
|
343
|
-
const secure = await
|
|
350
|
+
await this.checkTheme(req, reply)
|
|
351
|
+
await this.checkIconset(req, reply)
|
|
352
|
+
const secure = await this.checkUser(req, reply, 'waibuMpa')
|
|
344
353
|
if (!secure) return
|
|
345
|
-
await checkTeam
|
|
346
|
-
await checkXSite
|
|
354
|
+
await this.checkTeam(req, reply)
|
|
355
|
+
await this.checkXSite(req, reply)
|
|
347
356
|
}
|
|
348
357
|
}, {
|
|
349
358
|
level: 10,
|
|
350
359
|
name: 'waibuRestApi:preParsing',
|
|
351
360
|
handler: async function (req, reply) {
|
|
352
|
-
const secure = await
|
|
361
|
+
const secure = await this.checkUser(req, reply, 'waibuRestApi')
|
|
353
362
|
if (!secure) return
|
|
354
|
-
await checkTeam
|
|
355
|
-
await checkXSite
|
|
363
|
+
await this.checkTeam(req, reply)
|
|
364
|
+
await this.checkXSite(req, reply)
|
|
356
365
|
}
|
|
357
366
|
}, {
|
|
358
367
|
level: 10,
|
|
359
368
|
name: 'waibuStatic:preParsing',
|
|
360
369
|
handler: async function (req, reply) {
|
|
361
|
-
const secure = await
|
|
370
|
+
const secure = await this.checkUser(req, reply, 'waibuStatic')
|
|
362
371
|
if (!secure) return
|
|
363
|
-
await checkTeam
|
|
364
|
-
await checkXSite
|
|
372
|
+
await this.checkTeam(req, reply)
|
|
373
|
+
await this.checkXSite(req, reply)
|
|
365
374
|
}
|
|
366
375
|
}, {
|
|
367
376
|
name: 'waibu:afterAppBoot',
|
|
@@ -173,7 +173,9 @@
|
|
|
173
173
|
"hiddenCols": "Hidden Columns",
|
|
174
174
|
"models": "Models",
|
|
175
175
|
"siteIds": "Sites",
|
|
176
|
-
"allTeams": "All Teams?"
|
|
176
|
+
"allTeams": "All Teams?",
|
|
177
|
+
"condition": "Condition",
|
|
178
|
+
"hiddenFields": "Hide Fields"
|
|
177
179
|
},
|
|
178
180
|
"validation": {
|
|
179
181
|
"password": {
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -179,7 +179,9 @@
|
|
|
179
179
|
"hiddenCols": "Kolom Tersembunyi",
|
|
180
180
|
"models": "Model",
|
|
181
181
|
"siteIds": "Situs",
|
|
182
|
-
"allTeams": "Semua Tim?"
|
|
182
|
+
"allTeams": "Semua Tim?",
|
|
183
|
+
"condition": "Kondisi",
|
|
184
|
+
"hiddenFields": "Sembunyikan Kolom"
|
|
183
185
|
},
|
|
184
186
|
"validation": {
|
|
185
187
|
"password": {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
export async function latLngHook (body, options) {
|
|
2
|
+
const { isSet } = this.app.lib.aneka
|
|
3
|
+
const { round } = this.app.lib.aneka
|
|
4
|
+
if (!isSet(body[options.field])) return
|
|
5
|
+
body[options.field] = round(body[options.field], options.scale)
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
async function latLng (opts = {}) {
|
|
4
9
|
const { merge } = this.app.lib._
|
package/extend/dobo/model.js
CHANGED
|
@@ -76,11 +76,11 @@ async function model () {
|
|
|
76
76
|
},
|
|
77
77
|
'field,,50,true,true',
|
|
78
78
|
{
|
|
79
|
-
name: '
|
|
79
|
+
name: 'condition',
|
|
80
80
|
type: 'string',
|
|
81
81
|
maxLength: 20,
|
|
82
82
|
required: true,
|
|
83
|
-
values: ['IN', 'NIN'],
|
|
83
|
+
values: ['IN', 'NIN', 'EQ', 'NE', 'GT', 'GTE', 'LT', 'LTE', 'BETWEEN'],
|
|
84
84
|
default: 'IN'
|
|
85
85
|
},
|
|
86
86
|
'value,array,,,true',
|
package/extend/dobo/model.json
CHANGED
|
@@ -2,7 +2,7 @@ async function teamUser () {
|
|
|
2
2
|
return {
|
|
3
3
|
common: {
|
|
4
4
|
layout: [
|
|
5
|
-
{ name: 'meta', fields: ['id', 'createdAt', 'updatedAt'] },
|
|
5
|
+
{ name: 'meta', fields: ['id:3-md', 'createdAt:3-md', 'updatedAt:3-md', 'status:3-md'] },
|
|
6
6
|
{ name: 'general', fields: ['userId:6-md', 'teamId:6-md'] }
|
|
7
7
|
],
|
|
8
8
|
widget: {
|
|
@@ -20,7 +20,7 @@ async function teamUser () {
|
|
|
20
20
|
},
|
|
21
21
|
view: {
|
|
22
22
|
list: {
|
|
23
|
-
fields: ['userId', 'teamId', 'createdAt', 'updatedAt'],
|
|
23
|
+
fields: ['userId', 'teamId', 'status', 'createdAt', 'updatedAt'],
|
|
24
24
|
stat: {
|
|
25
25
|
aggregate: [
|
|
26
26
|
{ fields: ['userId'], group: 'userId', aggregate: ['count'] },
|
package/index.js
CHANGED
|
@@ -174,8 +174,7 @@ async function factory (pkgName) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
init = async () => {
|
|
177
|
-
|
|
178
|
-
this.downloadDir = `${getPluginDataDir(this.ns)}/download`
|
|
177
|
+
this.downloadDir = `${this.app.getPluginDataDir(this.ns)}/download`
|
|
179
178
|
this.app.lib.fs.ensureDirSync(this.downloadDir)
|
|
180
179
|
if (this.config.multiSite === true) {
|
|
181
180
|
this.config.multiSite = cloneDeep(defMultiSite)
|
|
@@ -526,9 +525,8 @@ async function factory (pkgName) {
|
|
|
526
525
|
}
|
|
527
526
|
|
|
528
527
|
pushDownload = async ({ description, worker, data, source, req, file, type }) => {
|
|
529
|
-
const {
|
|
530
|
-
const {
|
|
531
|
-
const { push } = getPlugin('bajoQueue')
|
|
528
|
+
const { createRecord } = this.app.getPlugin('waibuDb')
|
|
529
|
+
const { push } = this.app.getPlugin('bajoQueue')
|
|
532
530
|
description = description ?? file
|
|
533
531
|
const jobQueue = {
|
|
534
532
|
worker,
|
|
@@ -638,13 +636,21 @@ async function factory (pkgName) {
|
|
|
638
636
|
_getGuards = (inputs = []) => {
|
|
639
637
|
const { routePath } = this.app.waibu
|
|
640
638
|
const { orderBy } = this.app.lib._
|
|
641
|
-
const normal = orderBy(inputs.filter(input =>
|
|
642
|
-
|
|
643
|
-
|
|
639
|
+
const normal = orderBy(inputs.filter(input => {
|
|
640
|
+
try {
|
|
641
|
+
input.path = routePath(input.path)
|
|
642
|
+
} catch (err) {
|
|
643
|
+
return false
|
|
644
|
+
}
|
|
645
|
+
return input.path[0] !== '!'
|
|
644
646
|
}), ['weight', 'path'], ['desc', 'asc'])
|
|
645
|
-
const inverse = orderBy(inputs.filter(input =>
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
const inverse = orderBy(inputs.filter(input => {
|
|
648
|
+
try {
|
|
649
|
+
input.path = routePath(input.path)
|
|
650
|
+
} catch (err) {
|
|
651
|
+
return false
|
|
652
|
+
}
|
|
653
|
+
return input.path[0] === '!'
|
|
648
654
|
}), ['weight', 'path'], ['desc', 'asc'])
|
|
649
655
|
return [...normal, ...inverse]
|
|
650
656
|
}
|
|
@@ -673,7 +679,7 @@ async function factory (pkgName) {
|
|
|
673
679
|
getAttribGuards = async (reread) => {
|
|
674
680
|
if (!reread) return this.attribGuards
|
|
675
681
|
|
|
676
|
-
this.attribGuards = await this._fetchGuards('
|
|
682
|
+
this.attribGuards = await this._fetchGuards('Attrib')
|
|
677
683
|
return this.attribGuards
|
|
678
684
|
}
|
|
679
685
|
|
|
@@ -682,6 +688,155 @@ async function factory (pkgName) {
|
|
|
682
688
|
return asValue ? values.map(item => ({ value: item, text: item })) : values
|
|
683
689
|
}
|
|
684
690
|
|
|
691
|
+
pathsToCheck = (req) => {
|
|
692
|
+
const { uniq, without } = this.app.lib._
|
|
693
|
+
const items = [req.routeOptions.url, req.url].map(url => url.split('?')[0].split('#')[0])
|
|
694
|
+
return uniq(without(items, undefined, null))
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
checkIconset = async (req, reply) => {
|
|
698
|
+
const { get, isString } = this.app.lib._
|
|
699
|
+
const mpa = this.app.waibuMpa
|
|
700
|
+
|
|
701
|
+
if (!req.site) return
|
|
702
|
+
const siteIconset = get(req, 'site.setting.waibuMpa.iconset')
|
|
703
|
+
req.iconset = siteIconset ?? get(mpa, 'config.iconset.set', 'default')
|
|
704
|
+
const hiconset = req.headers['x-iconset']
|
|
705
|
+
if (isString(hiconset) && mpa.getIconset(hiconset)) req.iconset = hiconset
|
|
706
|
+
req.iconset = req.iconset ?? 'default'
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
checkTheme = async (req, reply) => {
|
|
710
|
+
const { get, isString } = this.app.lib._
|
|
711
|
+
const mpa = this.app.waibuMpa
|
|
712
|
+
|
|
713
|
+
if (!req.site) return
|
|
714
|
+
const siteTheme = get(req, 'site.setting.waibuMpa.theme')
|
|
715
|
+
req.theme = siteTheme ?? get(mpa, 'config.theme.set', 'default')
|
|
716
|
+
const htheme = req.headers['x-theme']
|
|
717
|
+
if (isString(htheme) && mpa.getTheme(htheme)) req.theme = htheme
|
|
718
|
+
req.theme = req.theme ?? 'default'
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
checkTeam = async (req, reply) => {
|
|
722
|
+
const { includes } = this.app.lib.aneka
|
|
723
|
+
const { outmatch } = this.app.lib
|
|
724
|
+
|
|
725
|
+
if (req.user.isAdmin) return
|
|
726
|
+
if (req.routeOptions.config.xSite && req.user.isXSiteAdmin) return
|
|
727
|
+
|
|
728
|
+
const teamIds = req.user.teams.map(item => item.id + '')
|
|
729
|
+
if (req.user.teams.map(item => item.alias).length === 0) throw this.error('accessDenied', { statusCode: 403 })
|
|
730
|
+
|
|
731
|
+
const paths = this.pathsToCheck(req)
|
|
732
|
+
const results = (await this.getSecureGuards()).filter(item => {
|
|
733
|
+
if (item.siteId !== req.site.id + '' || item.path[0] === '!') return false
|
|
734
|
+
return paths.some(outmatch([item.path]))
|
|
735
|
+
})
|
|
736
|
+
for (const result of results) {
|
|
737
|
+
if (result.allTeams) continue
|
|
738
|
+
if (!includes(teamIds, result.teamIds)) throw this.error('accessDenied', { statusCode: 403 })
|
|
739
|
+
}
|
|
740
|
+
// passed
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
checkUser = async (req, reply, source) => {
|
|
744
|
+
const { merge, isEmpty, camelCase, get } = this.app.lib._
|
|
745
|
+
const { routePath } = this.app.waibu
|
|
746
|
+
const userId = get(req, 'session.userId')
|
|
747
|
+
|
|
748
|
+
const setUser = async () => {
|
|
749
|
+
if (!userId) return
|
|
750
|
+
try {
|
|
751
|
+
const user = await this.getUserById(userId, req)
|
|
752
|
+
if (user) req.user = user
|
|
753
|
+
else req.session.userId = null
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.log(err)
|
|
756
|
+
req.session.userId = null
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (req.session) req.session.siteId = req.site.id
|
|
761
|
+
|
|
762
|
+
const webApp = get(req, 'routeOptions.config.webApp', 'waibu')
|
|
763
|
+
if (!req.routeOptions.url) {
|
|
764
|
+
if (!req.session) return
|
|
765
|
+
await setUser()
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const paths = this.pathsToCheck(req)
|
|
770
|
+
let guards = (await this.getAnonymousGuards()).filter(item => item.siteId === req.site.id + '')
|
|
771
|
+
const anonymous = this.checkRouteGuard(guards, paths)
|
|
772
|
+
if (anonymous) {
|
|
773
|
+
if (!userId) return false
|
|
774
|
+
req.session.ref = req.url
|
|
775
|
+
return reply.redirectTo(routePath(this.config.redirect.signout))
|
|
776
|
+
}
|
|
777
|
+
guards = (await this.getSecureGuards()).filter(item => item.siteId === req.site.id + '')
|
|
778
|
+
const secure = this.checkRouteGuard(guards, paths)
|
|
779
|
+
if (!secure) {
|
|
780
|
+
if (userId) await setUser()
|
|
781
|
+
return false // regular, unguarded path. Not secure & not anonymous path
|
|
782
|
+
}
|
|
783
|
+
if (userId) {
|
|
784
|
+
await setUser()
|
|
785
|
+
return secure
|
|
786
|
+
}
|
|
787
|
+
const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
|
|
788
|
+
const payload = silentOnError ? { noContent: true } : undefined
|
|
789
|
+
const authMethods = this.config.auth[webApp].methods ?? []
|
|
790
|
+
if (isEmpty(authMethods)) throw this.error('noAuthMethod', merge({ statusCode: 500 }, payload))
|
|
791
|
+
let success
|
|
792
|
+
for (const m of authMethods) {
|
|
793
|
+
const handler = this[camelCase(`verify ${m}`)]
|
|
794
|
+
if (!handler) throw this.error('invalidAuthMethod%s', m, merge({ statusCode: 500 }, payload))
|
|
795
|
+
const check = await handler(req, reply, source, payload)
|
|
796
|
+
if (check) {
|
|
797
|
+
success = check
|
|
798
|
+
break
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
|
|
802
|
+
return secure
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
checkXSite = async (req, reply) => {
|
|
806
|
+
const { get } = this.app.lib._
|
|
807
|
+
if (!this.config.multiSite.enabled) return
|
|
808
|
+
if (!get(req, 'routeOptions.config.xSite')) return
|
|
809
|
+
if (!get(req, 'user.isXSiteAdmin')) throw this.error('accessDenied', { statusCode: 403 })
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
parseNsSettings = (ns, setting, items) => {
|
|
813
|
+
const { trim, set, isPlainObject, isArray, isEmpty, find } = this.app.lib._
|
|
814
|
+
const { parseObject, dayjs } = this.app.lib
|
|
815
|
+
|
|
816
|
+
for (const item of items) {
|
|
817
|
+
if (item.ns === '_var' || ns === '_var') continue
|
|
818
|
+
let value = trim([item.value] ?? '')
|
|
819
|
+
if (value[0] === '#' && value[value.length - 1] === '#') {
|
|
820
|
+
const val = value.slice(1, -1)
|
|
821
|
+
const newValue = find(items, { ns: '_var', key: val })
|
|
822
|
+
if (newValue) value = newValue.value
|
|
823
|
+
}
|
|
824
|
+
if (['[', '{'].includes(value[0]) && [']', '}'].includes(value[value.length - 1])) {
|
|
825
|
+
try {
|
|
826
|
+
value = parseObject(JSON.parse(value))
|
|
827
|
+
} catch (err) {}
|
|
828
|
+
} else if (Number(value)) value = Number(value)
|
|
829
|
+
else if (['true', 'false'].includes(value)) value = value === 'true'
|
|
830
|
+
else if (item.key.endsWith('$in')) value = value.split('\n').map(v => v.trim())
|
|
831
|
+
else {
|
|
832
|
+
const dt = dayjs(value)
|
|
833
|
+
if (dt.isValid()) value = dt.toDate()
|
|
834
|
+
}
|
|
835
|
+
if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
|
|
836
|
+
set(setting, `${ns}.${item.key}`, value)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
685
840
|
createNewSite = createNewSite
|
|
686
841
|
removeSite = removeSite
|
|
687
842
|
getSite = getSite
|
package/lib/create-new-site.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
|
|
3
3
|
export async function getAllFixtures (alias) {
|
|
4
|
-
const {
|
|
4
|
+
const { readConfig } = this.app.bajo
|
|
5
5
|
const { isEmpty, omit, kebabCase, isString } = this.app.lib._
|
|
6
6
|
const { getModel } = this.app.dobo
|
|
7
7
|
const { fastGlob } = this.app.lib
|
|
@@ -17,7 +17,7 @@ export async function getAllFixtures (alias) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const formats = this.app.configHandlers.map(item => item.ext.slice(1))
|
|
20
|
-
const overrideBase = `${getPluginDataDir('sumba')}/create-new-site-fixtures/${alias}`
|
|
20
|
+
const overrideBase = `${this.app.getPluginDataDir('sumba')}/create-new-site-fixtures/${alias}`
|
|
21
21
|
const models = this.app.dobo.models.filter(m => {
|
|
22
22
|
const prop = m.properties.find(p => p.name === 'siteId')
|
|
23
23
|
return !!prop
|
package/lib/get-site.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { parseNsSettings } from './util.js'
|
|
2
|
-
|
|
3
1
|
async function getSite (input, byId = false) {
|
|
4
2
|
const { omit } = this.app.lib._
|
|
5
3
|
const { set: setCache, get: getCache } = this.app.bajoCache ?? {}
|
|
@@ -21,7 +19,7 @@ async function getSite (input, byId = false) {
|
|
|
21
19
|
const item = get(this, `app.${ns}.config.siteSetting`)
|
|
22
20
|
if (isSet(item)) defSetting[ns] = item
|
|
23
21
|
const items = filter(all, { ns })
|
|
24
|
-
parseNsSettings
|
|
22
|
+
this.parseNsSettings(ns, nsSetting, items)
|
|
25
23
|
}
|
|
26
24
|
site.setting = defaultsDeep({}, nsSetting, defSetting)
|
|
27
25
|
// additional fields
|
package/lib/get-user.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { parseNsSettings } from './util.js'
|
|
2
|
-
|
|
3
1
|
export async function mergeTeam (user, req) {
|
|
4
2
|
const { map, pick } = this.app.lib._
|
|
5
3
|
const { getModel } = this.app.dobo
|
|
6
4
|
user.teams = []
|
|
7
|
-
const query = { userId: user.id, siteId: user.siteId }
|
|
5
|
+
const query = { userId: user.id, siteId: user.siteId, status: 'ACTIVE' }
|
|
8
6
|
let mdl = getModel('SumbaTeamUser')
|
|
9
7
|
const userTeam = await mdl.findAllRecord({ query })
|
|
10
8
|
if (userTeam.length === 0) return
|
|
11
9
|
delete query.userId
|
|
12
10
|
query.id = { $in: map(userTeam, 'teamId') }
|
|
13
|
-
query.status = 'ACTIVE'
|
|
14
11
|
mdl = getModel('SumbaTeam')
|
|
15
12
|
const teams = await mdl.findAllRecord({ query })
|
|
16
13
|
if (teams.length > 0) {
|
|
17
14
|
delete query.id
|
|
18
15
|
delete query.status
|
|
19
|
-
query.siteId = user.siteId
|
|
20
16
|
query.teamId = { $in: teams.map(t => t.id + '') }
|
|
21
17
|
mdl = getModel('SumbaTeamSetting')
|
|
22
18
|
const items = await mdl.findAllRecord({ query })
|
|
@@ -25,7 +21,7 @@ export async function mergeTeam (user, req) {
|
|
|
25
21
|
const item = pick(team, ['id', 'alias'])
|
|
26
22
|
item.setting = {}
|
|
27
23
|
for (const ns of names) {
|
|
28
|
-
parseNsSettings
|
|
24
|
+
this.parseNsSettings(ns, item.setting, items.filter(s => s.teamId === (team.id + '')))
|
|
29
25
|
}
|
|
30
26
|
user.teams.push(item)
|
|
31
27
|
}
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-06-12
|
|
4
|
+
|
|
5
|
+
- [2.32.0] Necessary updates to ```bajo@2.18.0``` specs
|
|
6
|
+
- [2.32.0] Bug fix in ```_getGuards()```
|
|
7
|
+
- [2.32.0] Bug fix in ```getAttribGuards()```
|
|
8
|
+
- [2.32.0] Bug fix in ```hooks.js```
|
|
9
|
+
- [2.32.0] Change ```behavior``` to ```condition``` in ```SumbaModelGuard``` model
|
|
10
|
+
|
|
11
|
+
## 2026-06-11
|
|
12
|
+
|
|
13
|
+
- [2.31.0] Add ```status``` field on ```SumbaTeamUser```
|
|
14
|
+
- [2.31.0] Remove ```util.js``` as all functions now moved to base class
|
|
15
|
+
|
|
3
16
|
## 2026-06-10
|
|
4
17
|
|
|
5
18
|
- [2.30.0] Refactoring all guards
|
package/lib/util.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
export function parseNsSettings (ns, setting, items) {
|
|
2
|
-
const { trim, set, isPlainObject, isArray, isEmpty, find } = this.app.lib._
|
|
3
|
-
const { parseObject, dayjs } = this.app.lib
|
|
4
|
-
|
|
5
|
-
for (const item of items) {
|
|
6
|
-
if (item.ns === '_var' || ns === '_var') continue
|
|
7
|
-
let value = trim([item.value] ?? '')
|
|
8
|
-
if (value[0] === '#' && value[value.length - 1] === '#') {
|
|
9
|
-
const val = value.slice(1, -1)
|
|
10
|
-
const newValue = find(items, { ns: '_var', key: val })
|
|
11
|
-
if (newValue) value = newValue.value
|
|
12
|
-
}
|
|
13
|
-
if (['[', '{'].includes(value[0]) && [']', '}'].includes(value[value.length - 1])) {
|
|
14
|
-
try {
|
|
15
|
-
value = parseObject(JSON.parse(value))
|
|
16
|
-
} catch (err) {}
|
|
17
|
-
} else if (Number(value)) value = Number(value)
|
|
18
|
-
else if (['true', 'false'].includes(value)) value = value === 'true'
|
|
19
|
-
else if (item.key.endsWith('$in')) value = value.split('\n').map(v => v.trim())
|
|
20
|
-
else {
|
|
21
|
-
const dt = dayjs(value)
|
|
22
|
-
if (dt.isValid()) value = dt.toDate()
|
|
23
|
-
}
|
|
24
|
-
if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
|
|
25
|
-
set(setting, `${ns}.${item.key}`, value)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function pathsToCheck (req) {
|
|
30
|
-
const { uniq, without } = this.app.lib._
|
|
31
|
-
const items = [req.routeOptions.url, req.url].map(url => url.split('?')[0].split('#')[0])
|
|
32
|
-
return uniq(without(items, undefined, null))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function checkIconset (req, reply) {
|
|
36
|
-
const { get, isString } = this.app.lib._
|
|
37
|
-
const mpa = this.app.waibuMpa
|
|
38
|
-
|
|
39
|
-
if (!req.site) return
|
|
40
|
-
const siteIconset = get(req, 'site.setting.waibuMpa.iconset')
|
|
41
|
-
req.iconset = siteIconset ?? get(mpa, 'config.iconset.set', 'default')
|
|
42
|
-
const hiconset = req.headers['x-iconset']
|
|
43
|
-
if (isString(hiconset) && mpa.getIconset(hiconset)) req.iconset = hiconset
|
|
44
|
-
req.iconset = req.iconset ?? 'default'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function checkTheme (req, reply) {
|
|
48
|
-
const { get, isString } = this.app.lib._
|
|
49
|
-
const mpa = this.app.waibuMpa
|
|
50
|
-
|
|
51
|
-
if (!req.site) return
|
|
52
|
-
const siteTheme = get(req, 'site.setting.waibuMpa.theme')
|
|
53
|
-
req.theme = siteTheme ?? get(mpa, 'config.theme.set', 'default')
|
|
54
|
-
const htheme = req.headers['x-theme']
|
|
55
|
-
if (isString(htheme) && mpa.getTheme(htheme)) req.theme = htheme
|
|
56
|
-
req.theme = req.theme ?? 'default'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function checkTeam (req, reply) {
|
|
60
|
-
const { includes } = this.app.lib.aneka
|
|
61
|
-
const { outmatch } = this.app.lib
|
|
62
|
-
|
|
63
|
-
if (req.user.isAdmin) return
|
|
64
|
-
if (req.routeOptions.config.xSite && req.user.isXSiteAdmin) return
|
|
65
|
-
|
|
66
|
-
const teamIds = req.user.teams.map(item => item.id + '')
|
|
67
|
-
if (req.user.teams.map(item => item.alias).length === 0) throw this.error('accessDenied', { statusCode: 403 })
|
|
68
|
-
|
|
69
|
-
const paths = pathsToCheck.call(this, req)
|
|
70
|
-
const results = (await this.getSecureGuards()).filter(item => {
|
|
71
|
-
if (item.siteId !== req.site.id + '' || item.path[0] === '!') return false
|
|
72
|
-
return paths.some(outmatch([item.path]))
|
|
73
|
-
})
|
|
74
|
-
for (const result of results) {
|
|
75
|
-
if (result.allTeams) continue
|
|
76
|
-
if (!includes(teamIds, result.teamIds)) throw this.error('accessDenied', { statusCode: 403 })
|
|
77
|
-
}
|
|
78
|
-
// passed
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function checkUserId (req, reply, source) {
|
|
82
|
-
const { merge, isEmpty, camelCase, get } = this.app.lib._
|
|
83
|
-
const { routePath } = this.app.waibu
|
|
84
|
-
const userId = get(req, 'session.userId')
|
|
85
|
-
|
|
86
|
-
const setUser = async () => {
|
|
87
|
-
if (!userId) return
|
|
88
|
-
try {
|
|
89
|
-
const user = await this.getUserById(userId, req)
|
|
90
|
-
if (user) req.user = user
|
|
91
|
-
else req.session.userId = null
|
|
92
|
-
} catch (err) {
|
|
93
|
-
console.log(err)
|
|
94
|
-
req.session.userId = null
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (req.session) req.session.siteId = req.site.id
|
|
99
|
-
|
|
100
|
-
const webApp = get(req, 'routeOptions.config.webApp', 'waibu')
|
|
101
|
-
if (!req.routeOptions.url) {
|
|
102
|
-
if (!req.session) return
|
|
103
|
-
await setUser()
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const paths = pathsToCheck.call(this, req)
|
|
108
|
-
let guards = (await this.getAnonymousGuards()).filter(item => item.siteId === req.site.id + '')
|
|
109
|
-
const anonymous = this.checkRouteGuard(guards, paths)
|
|
110
|
-
if (anonymous) {
|
|
111
|
-
if (!userId) return false
|
|
112
|
-
req.session.ref = req.url
|
|
113
|
-
return reply.redirectTo(routePath(this.config.redirect.signout))
|
|
114
|
-
}
|
|
115
|
-
guards = (await this.getSecureGuards()).filter(item => item.siteId === req.site.id + '')
|
|
116
|
-
const secure = this.checkRouteGuard(guards, paths)
|
|
117
|
-
if (!secure) {
|
|
118
|
-
if (userId) await setUser()
|
|
119
|
-
return false // regular, unguarded path. Not secure & not anonymous path
|
|
120
|
-
}
|
|
121
|
-
if (userId) {
|
|
122
|
-
await setUser()
|
|
123
|
-
return secure
|
|
124
|
-
}
|
|
125
|
-
const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
|
|
126
|
-
const payload = silentOnError ? { noContent: true } : undefined
|
|
127
|
-
const authMethods = this.config.auth[webApp].methods ?? []
|
|
128
|
-
if (isEmpty(authMethods)) throw this.error('noAuthMethod', merge({ statusCode: 500 }, payload))
|
|
129
|
-
let success
|
|
130
|
-
for (const m of authMethods) {
|
|
131
|
-
const handler = this[camelCase(`verify ${m}`)]
|
|
132
|
-
if (!handler) throw this.error('invalidAuthMethod%s', m, merge({ statusCode: 500 }, payload))
|
|
133
|
-
const check = await handler(req, reply, source, payload)
|
|
134
|
-
if (check) {
|
|
135
|
-
success = check
|
|
136
|
-
break
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
|
|
140
|
-
return secure
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function latLngHook (body, options) {
|
|
144
|
-
const { isSet } = this.app.lib.aneka
|
|
145
|
-
const { round } = this.app.lib.aneka
|
|
146
|
-
if (!isSet(body[options.field])) return
|
|
147
|
-
body[options.field] = round(body[options.field], options.scale)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* If current route is an inter site route user is an inter site admin, then let it passed
|
|
152
|
-
*
|
|
153
|
-
* @param {Object} req - Request object
|
|
154
|
-
* @param {Object} reply - Reply object
|
|
155
|
-
* @returns
|
|
156
|
-
*/
|
|
157
|
-
export async function checkXSite (req, reply) {
|
|
158
|
-
const { get } = this.app.lib._
|
|
159
|
-
if (!this.config.multiSite.enabled) return
|
|
160
|
-
if (!get(req, 'routeOptions.config.xSite')) return
|
|
161
|
-
if (!get(req, 'user.isXSiteAdmin')) throw this.error('accessDenied', { statusCode: 403 })
|
|
162
|
-
}
|