sumba 2.11.1 → 2.13.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/waibu-mpa@pre-parsing.js +1 -4
- package/extend/bajo/hook/waibu-rest-api@pre-parsing.js +1 -2
- package/extend/bajo/hook/waibu-static@pre-parsing.js +1 -2
- package/extend/bajo/hook/waibu@after-app-boot.js +1 -2
- package/extend/bajo/hook/waibu@pre-parsing.js +7 -1
- package/extend/masohiSocketIo/middleware/server/auth.js +1 -1
- package/extend/waibuMpa/route/signin.js +1 -1
- package/extend/waibuRestApi/route/user/access-token/@type/create.js +2 -3
- package/index.js +20 -36
- package/lib/{collect-routes.js → collect.js} +21 -4
- package/lib/get-site.js +24 -11
- package/lib/get-user.js +56 -17
- package/lib/util.js +150 -2
- package/package.json +1 -1
- package/wiki/CHANGES.md +10 -0
- package/lib/check-iconset.js +0 -13
- package/lib/check-team.js +0 -19
- package/lib/check-theme.js +0 -13
- package/lib/check-user-id.js +0 -76
- package/lib/collect-redirects.js +0 -5
- package/lib/collect-team.js +0 -20
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import checkUserId from '../../../lib/
|
|
2
|
-
import checkTheme from '../../../lib/check-theme.js'
|
|
3
|
-
import checkIconset from '../../../lib/check-iconset.js'
|
|
4
|
-
import checkTeam from '../../../lib/check-team.js'
|
|
1
|
+
import { checkUserId, checkTeam, checkTheme, checkIconset } from '../../../lib/util.js'
|
|
5
2
|
|
|
6
3
|
const preParsing = {
|
|
7
4
|
level: 10,
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { checkNoRouteSetting, checkNoRouteSettingKey } from '../../../lib/util.js'
|
|
2
|
+
|
|
1
3
|
const preParsing = {
|
|
2
4
|
level: 10,
|
|
3
5
|
handler: async function (req, reply) {
|
|
4
|
-
|
|
6
|
+
const { get } = this.app.lib._
|
|
7
|
+
const { getHostname } = this.app.waibu
|
|
8
|
+
req.site = await this.getSite(getHostname(req))
|
|
9
|
+
const routes = get(req.site, checkNoRouteSettingKey)
|
|
10
|
+
checkNoRouteSetting.call(this, req, routes)
|
|
5
11
|
}
|
|
6
12
|
}
|
|
7
13
|
|
|
@@ -8,7 +8,7 @@ const auth = {
|
|
|
8
8
|
socket.join(camelCase(`site ${site.alias}`))
|
|
9
9
|
let user
|
|
10
10
|
if (session.userId) {
|
|
11
|
-
user = await this.
|
|
11
|
+
user = await this.getUserById(session.userId)
|
|
12
12
|
if (user) {
|
|
13
13
|
socket.join(camelCase(`user ${user.username}`))
|
|
14
14
|
for (const team of user.teams) {
|
|
@@ -7,7 +7,7 @@ const signin = {
|
|
|
7
7
|
let error
|
|
8
8
|
if (req.method === 'POST') {
|
|
9
9
|
try {
|
|
10
|
-
const user = await this.
|
|
10
|
+
const user = await this.getUserByUsernamePassword(username, password, req)
|
|
11
11
|
return await this.signin({ user, req, reply })
|
|
12
12
|
} catch (err) {
|
|
13
13
|
error = err
|
|
@@ -14,12 +14,11 @@ async function create () {
|
|
|
14
14
|
|
|
15
15
|
const handler = async function (req, reply) {
|
|
16
16
|
const { hash } = this.app.bajoExtra
|
|
17
|
-
const { getUserFromUsernamePassword, createJwtFromUserRecord } = this
|
|
18
17
|
|
|
19
18
|
if (!['api-key', 'jwt', 'apiKey'].includes(req.params.type)) throw this.error('invalidTokenType')
|
|
20
|
-
const rec = await
|
|
19
|
+
const rec = await this.getUserByUsernamePassword(req.body.username, req.body.password, req)
|
|
21
20
|
if (req.params.type === 'jwt') {
|
|
22
|
-
const jwt = await createJwtFromUserRecord(rec)
|
|
21
|
+
const jwt = await this.createJwtFromUserRecord(rec)
|
|
23
22
|
return { data: jwt }
|
|
24
23
|
}
|
|
25
24
|
return { data: { token: await hash(rec.salt) } }
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path'
|
|
|
2
2
|
import createNewSite from './lib/create-new-site.js'
|
|
3
3
|
import removeSite from './lib/remove-site.js'
|
|
4
4
|
import getSite from './lib/get-site.js'
|
|
5
|
-
import
|
|
5
|
+
import { getUserById, getUserByToken, getUserByUsernamePassword } from './lib/get-user.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Plugin factory
|
|
@@ -132,10 +132,15 @@ async function factory (pkgName) {
|
|
|
132
132
|
noWhitespace: false,
|
|
133
133
|
latinOnlyChars: false
|
|
134
134
|
}
|
|
135
|
+
},
|
|
136
|
+
cacheTtl: {
|
|
137
|
+
getSiteDur: '1m',
|
|
138
|
+
getUserByIdDur: '1m',
|
|
139
|
+
getUserByTokenDur: '1m'
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
this.unsafeUserFields = ['password']
|
|
138
|
-
this.selfBind(['createNewSite', 'removeSite', 'getSite', '
|
|
143
|
+
this.selfBind(['createNewSite', 'removeSite', 'getSite', 'getUserById', 'getUserByToken', 'getUserByUsernamePassword'])
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
init = async () => {
|
|
@@ -204,22 +209,6 @@ async function factory (pkgName) {
|
|
|
204
209
|
}]
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
getUserFromUsernamePassword = async (username = '', password = '', req) => {
|
|
208
|
-
const { importPkg } = this.app.bajo
|
|
209
|
-
const model = getModel('SumbaUser')
|
|
210
|
-
await model.validate({ username, password }, null, { partial: true, ns: ['sumba', 'dobo'], fields: ['username', 'password'] })
|
|
211
|
-
const bcrypt = await importPkg('bajoExtra:bcrypt')
|
|
212
|
-
|
|
213
|
-
const query = { username, provider: 'local', siteId: req.site.id }
|
|
214
|
-
const rows = await model.findRecord({ query }, { req, forceNoHidden: true, noHook: true })
|
|
215
|
-
if (rows.length === 0) throw this.error('validationError', { details: [{ field: 'username', error: 'Unknown username' }], statusCode: 401 })
|
|
216
|
-
const rec = rows[0]
|
|
217
|
-
if (rec.status !== 'ACTIVE') throw this.error('validationError', { details: ['User is inactive or temporarily disabled'], statusCode: 401 })
|
|
218
|
-
const verified = await bcrypt.compare(password, rec.password)
|
|
219
|
-
if (!verified) throw this.error('validationError', { details: [{ field: 'password', error: 'invalidPassword' }], statusCode: 401 })
|
|
220
|
-
return rec
|
|
221
|
-
}
|
|
222
|
-
|
|
223
212
|
createJwtFromUserRecord = async (rec) => {
|
|
224
213
|
const { importPkg } = this.app.bajo
|
|
225
214
|
const { dayjs } = this.app.lib
|
|
@@ -240,12 +229,11 @@ async function factory (pkgName) {
|
|
|
240
229
|
}
|
|
241
230
|
|
|
242
231
|
verifySession = async (req, reply, source, payload) => {
|
|
243
|
-
const { getUser } = this
|
|
244
232
|
const { routePath } = this.app.waibu
|
|
245
233
|
|
|
246
234
|
if (!req.session) return false
|
|
247
235
|
if (req.session.userId) {
|
|
248
|
-
req.user = await
|
|
236
|
+
req.user = await this.getUserById(req.session.userId, req)
|
|
249
237
|
return true
|
|
250
238
|
}
|
|
251
239
|
const redir = routePath(this.config.redirect.signin, req)
|
|
@@ -256,22 +244,18 @@ async function factory (pkgName) {
|
|
|
256
244
|
verifyApiKey = async (req, reply, source, payload) => {
|
|
257
245
|
const { merge } = this.app.lib._
|
|
258
246
|
const { isMd5, hash } = this.app.bajoExtra
|
|
259
|
-
const { getUser } = this
|
|
260
247
|
|
|
261
248
|
let token = await this._getToken('apiKey', req, source)
|
|
262
249
|
if (!isMd5(token)) return false
|
|
263
250
|
token = await hash(token)
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
req.user = await getUser(rows[0])
|
|
251
|
+
const user = await this.getUserByToken(token, req)
|
|
252
|
+
if (!user) throw this.error('invalidKey', merge({ statusCode: 401 }, payload))
|
|
253
|
+
if (user.status !== 'ACTIVE') throw this.error('userInactive', merge({ details: [{ field: 'status', error: 'inactive' }], statusCode: 401 }, payload))
|
|
254
|
+
req.user = user
|
|
269
255
|
return true
|
|
270
256
|
}
|
|
271
257
|
|
|
272
258
|
verifyBasic = async (req, reply, source, payload) => {
|
|
273
|
-
const { getUserFromUsernamePassword } = this
|
|
274
|
-
const { getUser } = this
|
|
275
259
|
const { isEmpty, merge } = this.app.lib._
|
|
276
260
|
|
|
277
261
|
const setHeader = async (setting, reply) => {
|
|
@@ -299,8 +283,7 @@ async function factory (pkgName) {
|
|
|
299
283
|
const decoded = Buffer.from(authInfo, 'base64').toString()
|
|
300
284
|
const [username, password] = decoded.split(':')
|
|
301
285
|
try {
|
|
302
|
-
|
|
303
|
-
req.user = await getUser(user)
|
|
286
|
+
req.user = await this.getUserByUsernamePassword(username, password, req)
|
|
304
287
|
} catch (err) {
|
|
305
288
|
if (err.statusCode === 401 && setting.realm) {
|
|
306
289
|
await setHeader(setting, reply)
|
|
@@ -313,7 +296,6 @@ async function factory (pkgName) {
|
|
|
313
296
|
|
|
314
297
|
verifyJwt = async (req, reply, source, payload) => {
|
|
315
298
|
const { importPkg } = this.app.bajo
|
|
316
|
-
const { getUser } = this
|
|
317
299
|
const { isEmpty, merge } = this.app.lib._
|
|
318
300
|
|
|
319
301
|
const fastJwt = await importPkg('bajoExtra:fast-jwt')
|
|
@@ -328,10 +310,10 @@ async function factory (pkgName) {
|
|
|
328
310
|
const decoded = await verifier(token)
|
|
329
311
|
const id = decoded.payload.uid
|
|
330
312
|
try {
|
|
331
|
-
const
|
|
332
|
-
if (!
|
|
333
|
-
if (
|
|
334
|
-
req.user =
|
|
313
|
+
const user = await this.getUserById(id, req)
|
|
314
|
+
if (!user) throw this.error('invalidToken', { statusCode: 401 })
|
|
315
|
+
if (user.status !== 'ACTIVE') throw this.error('userInactive', { details: [{ field: 'status', error: 'inactive' }], statusCode: 401 })
|
|
316
|
+
req.user = user
|
|
335
317
|
} catch (err) {
|
|
336
318
|
merge(err, payload)
|
|
337
319
|
throw err
|
|
@@ -494,7 +476,9 @@ async function factory (pkgName) {
|
|
|
494
476
|
createNewSite = createNewSite
|
|
495
477
|
removeSite = removeSite
|
|
496
478
|
getSite = getSite
|
|
497
|
-
|
|
479
|
+
getUserById = getUserById
|
|
480
|
+
getUserByToken = getUserByToken
|
|
481
|
+
getUserByUsernamePassword = getUserByUsernamePassword
|
|
498
482
|
}
|
|
499
483
|
|
|
500
484
|
return Sumba
|
|
@@ -23,21 +23,38 @@ export async function collect ({ type = '', handler, container, file, ns, dir })
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function
|
|
26
|
+
function collectHandler (item, keys = []) {
|
|
27
27
|
const { isString } = this.app.lib._
|
|
28
|
-
for (const k of
|
|
28
|
+
for (const k of keys) {
|
|
29
29
|
item[k] = item[k] ?? []
|
|
30
30
|
if (isString(item[k])) item[k] = item[k].split(',')
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function collectRoutes (type) {
|
|
34
|
+
export async function collectRoutes (type) {
|
|
35
35
|
const { eachPlugins } = this.app.bajo
|
|
36
36
|
const me = this
|
|
37
|
+
|
|
38
|
+
function handler (item) {
|
|
39
|
+
collectHandler.call(this, item, ['methods'])
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
await eachPlugins(async function ({ file, dir }) {
|
|
38
43
|
const { ns } = this
|
|
39
44
|
await collect.call(me, { type, container: 'Routes', handler, file, ns, dir })
|
|
40
45
|
}, { glob: `route/${type}.*`, prefix: this.ns })
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
export
|
|
48
|
+
export async function collectTeam () {
|
|
49
|
+
const { eachPlugins } = this.app.bajo
|
|
50
|
+
const me = this
|
|
51
|
+
|
|
52
|
+
function handler (item) {
|
|
53
|
+
collectHandler.call(this, item, ['methods', 'teams', 'features'])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await eachPlugins(async function ({ file, dir }) {
|
|
57
|
+
const { ns } = this
|
|
58
|
+
await collect.call(me, { type: 'team', container: 'Routes', handler, file, ns, dir })
|
|
59
|
+
}, { glob: 'route/team.*', prefix: this.ns })
|
|
60
|
+
}
|
package/lib/get-site.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { parseNsSettings } from './util.js'
|
|
2
2
|
|
|
3
|
-
async function getSite (
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const { getHostname } = this.app.waibu
|
|
3
|
+
async function getSite (input, byId = false) {
|
|
4
|
+
const { omit } = this.app.lib._
|
|
5
|
+
const { set: setCache, get: getCache } = this.app.bajoCache ?? {}
|
|
7
6
|
const omitted = ['status']
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
let site = {}
|
|
10
9
|
const mergeSetting = async (site) => {
|
|
11
10
|
const { defaultsDeep, isSet } = this.app.lib.aneka
|
|
12
11
|
const { get, filter } = this.app.lib._
|
|
@@ -28,21 +27,32 @@ async function getSite (req) {
|
|
|
28
27
|
// additional fields
|
|
29
28
|
const country = await this.app.dobo.getModel('CdbCountry').getRecord(site.country, { noHook: true })
|
|
30
29
|
site.countryName = (country ?? {}).name ?? site.country
|
|
31
|
-
await runHook(`${this.ns}:afterGetSite`, req, site)
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
let site = {}
|
|
35
|
-
|
|
36
32
|
const multiSite = this.config.multiSite === true ? { enabled: true, catchAll: 'default' } : this.config.multiSite
|
|
37
33
|
if (!multiSite.enabled) {
|
|
38
|
-
const
|
|
34
|
+
const filter = { query: { alias: 'default' } }
|
|
35
|
+
const key = 'getSite:default'
|
|
36
|
+
if (getCache) {
|
|
37
|
+
site = await getCache({ key })
|
|
38
|
+
if (site) return site
|
|
39
|
+
}
|
|
40
|
+
const resp = await this.app.dobo.getModel('SumbaSite').findOneRecord(filter, { noHook: true })
|
|
39
41
|
site = omit(resp, omitted)
|
|
40
42
|
await mergeSetting(site)
|
|
43
|
+
if (setCache) {
|
|
44
|
+
await setCache({ key, value: site, ttl: this.config.cacheTtl.getSiteDur })
|
|
45
|
+
}
|
|
41
46
|
return site
|
|
42
47
|
}
|
|
43
48
|
let query
|
|
44
|
-
if (
|
|
45
|
-
else query = { hostname:
|
|
49
|
+
if (byId) query = { id: input }
|
|
50
|
+
else query = { hostname: input }
|
|
51
|
+
const key = `getSite:multiSite:${input}`
|
|
52
|
+
if (getCache) {
|
|
53
|
+
site = await getCache({ key })
|
|
54
|
+
if (site) return JSON.parse(site)
|
|
55
|
+
}
|
|
46
56
|
let row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
|
|
47
57
|
if (!row) {
|
|
48
58
|
if (multiSite.catchAll) {
|
|
@@ -54,6 +64,9 @@ async function getSite (req) {
|
|
|
54
64
|
if (row.status !== 'ACTIVE') throw this.error('siteInactiveInfo')
|
|
55
65
|
site = omit(row, omitted)
|
|
56
66
|
await mergeSetting(site)
|
|
67
|
+
if (setCache) {
|
|
68
|
+
await setCache({ key, value: JSON.stringify(site), ttl: this.config.cacheTtl.getSiteDur })
|
|
69
|
+
}
|
|
57
70
|
return site
|
|
58
71
|
}
|
|
59
72
|
|
package/lib/get-user.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { parseNsSettings } from './util.js'
|
|
2
2
|
|
|
3
|
-
async function
|
|
4
|
-
const {
|
|
5
|
-
const { map, pick, omit, isPlainObject } = this.app.lib._
|
|
3
|
+
export async function mergeTeam (user) {
|
|
4
|
+
const { map, pick } = this.app.lib._
|
|
6
5
|
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
6
|
user.teams = []
|
|
18
7
|
const query = { userId: user.id, siteId: user.siteId }
|
|
19
8
|
let mdl = getModel('SumbaTeamUser')
|
|
@@ -25,7 +14,6 @@ async function getUser (rec, safe = true) {
|
|
|
25
14
|
mdl = getModel('SumbaTeam')
|
|
26
15
|
const teams = await mdl.findAllRecord({ query })
|
|
27
16
|
if (teams.length > 0) {
|
|
28
|
-
// setting
|
|
29
17
|
delete query.id
|
|
30
18
|
delete query.status
|
|
31
19
|
query.siteId = user.siteId
|
|
@@ -42,9 +30,60 @@ async function getUser (rec, safe = true) {
|
|
|
42
30
|
user.teams.push(item)
|
|
43
31
|
}
|
|
44
32
|
}
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
for (const field of this.unsafeUserFields) {
|
|
34
|
+
delete user[field]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getUserById (id, req) {
|
|
39
|
+
const { getModel } = this.app.dobo
|
|
40
|
+
const { set: setCache, get: getCache } = this.app.bajoCache ?? {}
|
|
41
|
+
const key = `getUserById:${id}`
|
|
42
|
+
let user
|
|
43
|
+
if (getCache) {
|
|
44
|
+
user = await getCache({ key })
|
|
45
|
+
if (user) return JSON.parse(user)
|
|
46
|
+
}
|
|
47
|
+
user = await getModel('SumbaUser').getRecord(id, { noHook: true, throwNotFound: false, req })
|
|
48
|
+
if (!user) return
|
|
49
|
+
await mergeTeam.call(this, user)
|
|
50
|
+
if (setCache) {
|
|
51
|
+
await setCache({ key, value: JSON.stringify(user), ttl: this.config.cacheTtl.getUserByIdDur })
|
|
52
|
+
}
|
|
53
|
+
return user
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getUserByToken (token, req) {
|
|
57
|
+
const { getModel } = this.app.dobo
|
|
58
|
+
const { set: setCache, get: getCache } = this.app.bajoCache ?? {}
|
|
59
|
+
const key = `getUserByToken:${token}`
|
|
60
|
+
let user
|
|
61
|
+
if (getCache) {
|
|
62
|
+
user = await getCache({ key })
|
|
63
|
+
if (user) return JSON.parse(user)
|
|
64
|
+
}
|
|
65
|
+
user = await getModel('SumbaUser').findOneRecord({ query: { token } }, { noHook: true, throwNotFound: false, req })
|
|
66
|
+
if (!user) return
|
|
67
|
+
await mergeTeam.call(this, user)
|
|
68
|
+
if (setCache) {
|
|
69
|
+
await setCache({ key, value: JSON.stringify(user), ttl: this.config.cacheTtl.getUserByTokenDur })
|
|
70
|
+
}
|
|
47
71
|
return user
|
|
48
72
|
}
|
|
49
73
|
|
|
50
|
-
export
|
|
74
|
+
export async function getUserByUsernamePassword (username = '', password = '', req) {
|
|
75
|
+
const { getModel } = this.app.dobo
|
|
76
|
+
const { importPkg } = this.app.bajo
|
|
77
|
+
const model = getModel('SumbaUser')
|
|
78
|
+
await model.validate({ username, password }, null, { partial: true, ns: ['sumba', 'dobo'], fields: ['username', 'password'] })
|
|
79
|
+
const bcrypt = await importPkg('bajoExtra:bcrypt')
|
|
80
|
+
|
|
81
|
+
const query = { username, provider: 'local', siteId: req.site.id }
|
|
82
|
+
const user = await model.findOneRecord({ query }, { req, forceNoHidden: true, noHook: true })
|
|
83
|
+
if (!user) throw this.error('validationError', { details: [{ field: 'username', error: 'Unknown username' }], statusCode: 401 })
|
|
84
|
+
if (user.status !== 'ACTIVE') throw this.error('validationError', { details: ['User is inactive or temporarily disabled'], statusCode: 401 })
|
|
85
|
+
const verified = await bcrypt.compare(password, user.password)
|
|
86
|
+
if (!verified) throw this.error('validationError', { details: [{ field: 'password', error: 'invalidPassword' }], statusCode: 401 })
|
|
87
|
+
await mergeTeam.call(this, user)
|
|
88
|
+
return user
|
|
89
|
+
}
|
package/lib/util.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export const checkNoRouteSettingKey = 'setting.waibu.noRoutes.$in'
|
|
2
|
+
|
|
1
3
|
export function parseNsSettings (ns, setting, items) {
|
|
2
|
-
const { trim, set, isPlainObject, isArray, isEmpty, find } = this.app.lib._
|
|
4
|
+
const { trim, set, isPlainObject, isArray, isEmpty, find, get, isString } = this.app.lib._
|
|
3
5
|
const { parseObject, dayjs } = this.app.lib
|
|
6
|
+
const { routePath } = this.app.waibu
|
|
4
7
|
|
|
5
8
|
for (const item of items) {
|
|
6
9
|
if (item.ns === '_var' || ns === '_var') continue
|
|
@@ -16,7 +19,7 @@ export function parseNsSettings (ns, setting, items) {
|
|
|
16
19
|
} catch (err) {}
|
|
17
20
|
} else if (Number(value)) value = Number(value)
|
|
18
21
|
else if (['true', 'false'].includes(value)) value = value === 'true'
|
|
19
|
-
else if (item.key.endsWith('$in')) value = value.split('
|
|
22
|
+
else if (item.key.endsWith('$in')) value = value.split('\n').map(v => v.trim())
|
|
20
23
|
else {
|
|
21
24
|
const dt = dayjs(value)
|
|
22
25
|
if (dt.isValid()) value = dt.toDate()
|
|
@@ -24,4 +27,149 @@ export function parseNsSettings (ns, setting, items) {
|
|
|
24
27
|
if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
|
|
25
28
|
set(setting, `${ns}.${item.key}`, value)
|
|
26
29
|
}
|
|
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
|
+
}
|
|
55
|
+
|
|
56
|
+
export function pathsToCheck (req, withHome) {
|
|
57
|
+
const { uniq, without } = this.app.lib._
|
|
58
|
+
return uniq(without([req.routeOptions.url, req.url], undefined, null))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function checkIconset (req, reply) {
|
|
62
|
+
const { get, isString } = this.app.lib._
|
|
63
|
+
const mpa = this.app.waibuMpa
|
|
64
|
+
|
|
65
|
+
if (!req.site) return
|
|
66
|
+
const siteIconset = get(req, 'site.setting.waibuMpa.iconset')
|
|
67
|
+
req.iconset = siteIconset ?? get(mpa, 'config.iconset.set', 'default')
|
|
68
|
+
const hiconset = req.headers['x-iconset']
|
|
69
|
+
if (isString(hiconset) && mpa.getIconset(hiconset)) req.iconset = hiconset
|
|
70
|
+
req.iconset = req.iconset ?? 'default'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function checkTheme (req, reply) {
|
|
74
|
+
const { get, isString } = this.app.lib._
|
|
75
|
+
const mpa = this.app.waibuMpa
|
|
76
|
+
|
|
77
|
+
if (!req.site) return
|
|
78
|
+
const siteTheme = get(req, 'site.setting.waibuMpa.theme')
|
|
79
|
+
req.theme = siteTheme ?? get(mpa, 'config.theme.set', 'default')
|
|
80
|
+
const htheme = req.headers['x-theme']
|
|
81
|
+
if (isString(htheme) && mpa.getTheme(htheme)) req.theme = htheme
|
|
82
|
+
req.theme = req.theme ?? 'default'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function checkTeam (req, reply, source) {
|
|
86
|
+
const { get, isEmpty } = this.app.lib._
|
|
87
|
+
if (!req.user) return
|
|
88
|
+
const { map } = this.app.lib._
|
|
89
|
+
const paths = pathsToCheck.call(this, req, true)
|
|
90
|
+
const teams = map(req.user.teams, 'alias')
|
|
91
|
+
let match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamRoutes })
|
|
92
|
+
if (!match) match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamNegRoutes })
|
|
93
|
+
if (!match) {
|
|
94
|
+
const guards = map(this.teamRoutes, 'path')
|
|
95
|
+
match = !this.checkPathsByGuard({ paths, guards })
|
|
96
|
+
}
|
|
97
|
+
if (!match) throw this.error('accessDenied', { statusCode: 403 })
|
|
98
|
+
const routes = []
|
|
99
|
+
for (const team of (req.user.teams ?? [])) {
|
|
100
|
+
const items = get(team, checkNoRouteSettingKey, [])
|
|
101
|
+
if (isEmpty(items)) continue
|
|
102
|
+
routes.push(...items)
|
|
103
|
+
}
|
|
104
|
+
checkNoRouteSetting.call(this, req, routes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function checkUserId (req, reply, source) {
|
|
108
|
+
const { merge, isEmpty, camelCase, get } = this.app.lib._
|
|
109
|
+
const { routePath } = this.app.waibu
|
|
110
|
+
const userId = get(req, 'session.userId')
|
|
111
|
+
|
|
112
|
+
const setUser = async () => {
|
|
113
|
+
const id = get(req, 'session.userId')
|
|
114
|
+
if (!id) return
|
|
115
|
+
try {
|
|
116
|
+
const user = await this.getUserById(id, req)
|
|
117
|
+
if (user) req.user = user
|
|
118
|
+
else req.session.userId = null
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.log(err)
|
|
121
|
+
req.session.userId = null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (req.session) req.session.siteId = req.site.id
|
|
126
|
+
|
|
127
|
+
const webApp = get(req, 'routeOptions.config.webApp', 'waibu')
|
|
128
|
+
if (!req.routeOptions.url) {
|
|
129
|
+
if (!req.session) return
|
|
130
|
+
await setUser()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const paths = pathsToCheck.call(this, req)
|
|
135
|
+
let securePath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureRoutes })
|
|
136
|
+
if (securePath) {
|
|
137
|
+
const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureNegRoutes })
|
|
138
|
+
if (neg) securePath = undefined
|
|
139
|
+
}
|
|
140
|
+
let anonymousPath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousRoutes })
|
|
141
|
+
if (anonymousPath) {
|
|
142
|
+
const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousNegRoutes })
|
|
143
|
+
if (neg) anonymousPath = undefined
|
|
144
|
+
}
|
|
145
|
+
if (!securePath && !anonymousPath) {
|
|
146
|
+
if (userId) await setUser()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (anonymousPath) {
|
|
150
|
+
if (!userId) return
|
|
151
|
+
req.session.ref = req.url
|
|
152
|
+
return reply.redirectTo(routePath(this.config.redirect.signout))
|
|
153
|
+
}
|
|
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
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
|
|
174
|
+
}
|
|
27
175
|
}
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-03-25
|
|
4
|
+
|
|
5
|
+
- [2.13.0] Add bajo cache handling for ```getUser*()``` and ```getSite()```
|
|
6
|
+
|
|
7
|
+
## 2026-03-22
|
|
8
|
+
|
|
9
|
+
- [2.12.0] Add no route settings sitewise
|
|
10
|
+
- [2.12.0] Add no route settings for teams
|
|
11
|
+
- [2.12.0] Some organizatorial changes in module files
|
|
12
|
+
|
|
3
13
|
## 2026-03-22
|
|
4
14
|
|
|
5
15
|
- [2.11.0] Add ```Team Setting``` feature
|
package/lib/check-iconset.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
async function checkIconset (req, reply) {
|
|
2
|
-
const { get, isString } = this.app.lib._
|
|
3
|
-
const mpa = this.app.waibuMpa
|
|
4
|
-
|
|
5
|
-
if (!req.site) return
|
|
6
|
-
const siteIconset = get(req, 'site.setting.waibuMpa.iconset')
|
|
7
|
-
req.iconset = siteIconset ?? get(mpa, 'config.iconset.set', 'default')
|
|
8
|
-
const hiconset = req.headers['x-iconset']
|
|
9
|
-
if (isString(hiconset) && mpa.getIconset(hiconset)) req.iconset = hiconset
|
|
10
|
-
req.iconset = req.iconset ?? 'default'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default checkIconset
|
package/lib/check-team.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { pathsToCheck } from './check-user-id.js'
|
|
2
|
-
|
|
3
|
-
async function checkTeam (req, reply, source) {
|
|
4
|
-
if (!req.user) return
|
|
5
|
-
const { map } = this.app.lib._
|
|
6
|
-
const paths = pathsToCheck.call(this, req, true)
|
|
7
|
-
const teams = map(req.user.teams, 'alias')
|
|
8
|
-
const match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamRoutes })
|
|
9
|
-
if (match) return
|
|
10
|
-
const negMatch = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamNegRoutes })
|
|
11
|
-
if (negMatch) return
|
|
12
|
-
const guards = map(this.teamRoutes, 'path')
|
|
13
|
-
// fallback
|
|
14
|
-
const guarded = this.checkPathsByGuard({ paths, guards })
|
|
15
|
-
if (!guarded) return
|
|
16
|
-
throw this.error('accessDenied', { statusCode: 403 })
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default checkTeam
|
package/lib/check-theme.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
async function checkTheme (req, reply) {
|
|
2
|
-
const { get, isString } = this.app.lib._
|
|
3
|
-
const mpa = this.app.waibuMpa
|
|
4
|
-
|
|
5
|
-
if (!req.site) return
|
|
6
|
-
const siteTheme = get(req, 'site.setting.waibuMpa.theme')
|
|
7
|
-
req.theme = siteTheme ?? get(mpa, 'config.theme.set', 'default')
|
|
8
|
-
const htheme = req.headers['x-theme']
|
|
9
|
-
if (isString(htheme) && mpa.getTheme(htheme)) req.theme = htheme
|
|
10
|
-
req.theme = req.theme ?? 'default'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default checkTheme
|
package/lib/check-user-id.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export function pathsToCheck (req, withHome) {
|
|
2
|
-
const { uniq, without } = this.app.lib._
|
|
3
|
-
return uniq(without([req.routeOptions.url, req.url], undefined, null))
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
async function setUser (req) {
|
|
7
|
-
const { get } = this.app.lib._
|
|
8
|
-
const id = get(req, 'session.userId')
|
|
9
|
-
if (!id) return
|
|
10
|
-
try {
|
|
11
|
-
const user = await this.getUser(id)
|
|
12
|
-
if (user) req.user = user
|
|
13
|
-
else req.session.userId = null
|
|
14
|
-
} catch (err) {
|
|
15
|
-
console.log(err)
|
|
16
|
-
req.session.userId = null
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function checkUserId (req, reply, source) {
|
|
21
|
-
const { merge, isEmpty, camelCase, get } = this.app.lib._
|
|
22
|
-
const { routePath } = this.app.waibu
|
|
23
|
-
const userId = get(req, 'session.userId')
|
|
24
|
-
if (req.session) req.session.siteId = req.site.id
|
|
25
|
-
|
|
26
|
-
const webApp = get(req, 'routeOptions.config.webApp', 'waibu')
|
|
27
|
-
if (!req.routeOptions.url) {
|
|
28
|
-
if (!req.session) return
|
|
29
|
-
await setUser.call(this, req)
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const paths = pathsToCheck.call(this, req)
|
|
34
|
-
let securePath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureRoutes })
|
|
35
|
-
if (securePath) {
|
|
36
|
-
const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.secureNegRoutes })
|
|
37
|
-
if (neg) securePath = undefined
|
|
38
|
-
}
|
|
39
|
-
let anonymousPath = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousRoutes })
|
|
40
|
-
if (anonymousPath) {
|
|
41
|
-
const neg = await this.checkPathsByRoute({ paths, method: req.method, guards: this.anonymousNegRoutes })
|
|
42
|
-
if (neg) anonymousPath = undefined
|
|
43
|
-
}
|
|
44
|
-
if (!securePath && !anonymousPath) {
|
|
45
|
-
if (userId) await setUser.call(this, req)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
if (anonymousPath) {
|
|
49
|
-
if (!userId) return
|
|
50
|
-
req.session.ref = req.url
|
|
51
|
-
return reply.redirectTo(routePath(this.config.redirect.signout))
|
|
52
|
-
}
|
|
53
|
-
if (securePath) {
|
|
54
|
-
if (userId) {
|
|
55
|
-
await setUser.call(this, req)
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
const silentOnError = this.config.auth[webApp].silentOnError ?? this.config.auth.common.silentOnError
|
|
59
|
-
const payload = silentOnError ? { noContent: true } : undefined
|
|
60
|
-
const authMethods = this.config.auth[webApp].methods ?? []
|
|
61
|
-
if (isEmpty(authMethods)) throw this.error('noAuthMethod', merge({ statusCode: 500 }, payload))
|
|
62
|
-
let success
|
|
63
|
-
for (const m of authMethods) {
|
|
64
|
-
const handler = this[camelCase(`verify ${m}`)]
|
|
65
|
-
if (!handler) throw this.error('invalidAuthMethod%s', m, merge({ statusCode: 500 }, payload))
|
|
66
|
-
const check = await handler(req, reply, source, payload)
|
|
67
|
-
if (check) {
|
|
68
|
-
success = check
|
|
69
|
-
break
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (!success) throw this.error('accessDeniedNoAuth', merge({ statusCode: 403 }, payload))
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export default checkUserId
|
package/lib/collect-redirects.js
DELETED
package/lib/collect-team.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { collect } from './collect-routes.js'
|
|
2
|
-
|
|
3
|
-
function handler (item) {
|
|
4
|
-
const { isString } = this.app.lib._
|
|
5
|
-
for (const k of ['methods', 'teams', 'features']) {
|
|
6
|
-
item[k] = item[k] ?? []
|
|
7
|
-
if (isString(item[k])) item[k] = item[k].split(',')
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async function collectTeam () {
|
|
12
|
-
const { eachPlugins } = this.app.bajo
|
|
13
|
-
const me = this
|
|
14
|
-
await eachPlugins(async function ({ file, dir }) {
|
|
15
|
-
const { ns } = this
|
|
16
|
-
await collect.call(me, { type: 'team', container: 'Routes', handler, file, ns, dir })
|
|
17
|
-
}, { glob: 'route/team.*', prefix: this.ns })
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export default collectTeam
|