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