expressa 1.4.6 → 2.0.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/.eslintrc.json +1 -1
- package/controllers/collections.js +66 -8
- package/controllers/users.js +52 -50
- package/listeners.js +5 -10
- package/listeners_collection_permissions.js +1 -4
- package/listeners_users.js +92 -88
- package/listeners_validation.js +43 -78
- package/modules/collections/collections.js +10 -2
- package/modules/permissions/permissions.js +2 -8
- package/package.json +1 -1
- package/test/0-install.js +11 -0
- package/test/collections.querying.js +10 -10
- package/test/test.js +1 -1
- package/test/testutils.js +54 -2
- package/test/users.js +119 -0
- package/util.js +47 -43
package/.eslintrc.json
CHANGED
|
@@ -10,6 +10,55 @@ function assertValidCollection(req) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
async function validateDocumentOwner(req, doc) {
|
|
14
|
+
if (!doc.meta.owner_collection) {
|
|
15
|
+
throw new util.ApiError(417, 'no owner_collection for owner found')
|
|
16
|
+
}
|
|
17
|
+
if (doc._id !== doc.meta.owner) {
|
|
18
|
+
const count = await req.db[doc.meta.owner_collection].count({ _id: doc.meta.owner }, undefined, 1)
|
|
19
|
+
if (!count) {
|
|
20
|
+
throw new util.ApiError(417, 'invalid owner')
|
|
21
|
+
}
|
|
22
|
+
} else if (req.params.collection !== doc.meta.owner_collection) {
|
|
23
|
+
throw new util.ApiError(417, 'invalid owner collection')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function setDocumentOwner(req, doc) {
|
|
28
|
+
const schema = await req.db.collection.get(req.params.collection)
|
|
29
|
+
if (schema.documentsHaveOwners) {
|
|
30
|
+
util.addIdIfMissing(doc)
|
|
31
|
+
doc.meta ??= {}
|
|
32
|
+
if (req.uid) {
|
|
33
|
+
// request done by logged in user
|
|
34
|
+
if (!doc.meta.owner) {
|
|
35
|
+
// no owner - make logged in user as owner
|
|
36
|
+
doc.meta.owner = req.user._id
|
|
37
|
+
doc.meta.owner_collection = req.ucollection
|
|
38
|
+
} else {
|
|
39
|
+
if (!req.hasPermission(`${req.params.collection}: modify owner`)) {
|
|
40
|
+
// has an owner - but if no permission to modify owner then override owner to logged in user
|
|
41
|
+
doc.meta.owner = req.user._id
|
|
42
|
+
doc.meta.owner_collection = req.ucollection
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
// request done by NOT logged in user
|
|
47
|
+
if (schema.enableLogin) {
|
|
48
|
+
// is a user/login collection so make self owner
|
|
49
|
+
doc.meta.owner = doc._id
|
|
50
|
+
doc.meta.owner_collection = req.params.collection
|
|
51
|
+
} else {
|
|
52
|
+
// is a normal collection
|
|
53
|
+
debug('unable to find valid owner')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (doc.meta.owner) {
|
|
57
|
+
await validateDocumentOwner(req, doc)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
13
62
|
exports.getSchema = async function (req) {
|
|
14
63
|
const collection = await req.db.collection.get(req.params.collection)
|
|
15
64
|
await util.notify('get', req, 'schemas', collection)
|
|
@@ -23,13 +72,14 @@ exports.get = async function (req) {
|
|
|
23
72
|
}
|
|
24
73
|
|
|
25
74
|
const fields = req.query.fields ? JSON.parse(req.query.fields) : null
|
|
75
|
+
delete req.query.fields
|
|
26
76
|
|
|
27
77
|
let data, query
|
|
28
78
|
if (req.query.query) {
|
|
29
79
|
query = JSON.parse(req.query.query)
|
|
30
80
|
}
|
|
31
81
|
else {
|
|
32
|
-
const { skip, offset, limit, page, pageitems, pagemetadisable, orderby,
|
|
82
|
+
const { skip, offset, limit, page, pageitems, pagemetadisable, orderby, ...params } = req.query // eslint-disable-line no-unused-vars
|
|
33
83
|
query = queryStringParser.parse(params)
|
|
34
84
|
}
|
|
35
85
|
if (req.query.orderby) {
|
|
@@ -41,7 +91,8 @@ exports.get = async function (req) {
|
|
|
41
91
|
// scenario where logged in user can only retrieve own docs
|
|
42
92
|
if (!req.hasPermission(`${req.params.collection}: view`) && req.hasPermission(`${req.params.collection}: view own`)) {
|
|
43
93
|
query['meta.owner'] = req.uid
|
|
44
|
-
fields
|
|
94
|
+
// if fields is null then all fields are requested
|
|
95
|
+
if (fields) fields['meta.owner'] = 1
|
|
45
96
|
}
|
|
46
97
|
}
|
|
47
98
|
|
|
@@ -94,7 +145,7 @@ exports.insert = async function (req) {
|
|
|
94
145
|
const data = req.body
|
|
95
146
|
req.customResponseData = req.customResponseData || {}
|
|
96
147
|
await util.notify('post', req, req.params.collection, data)
|
|
97
|
-
|
|
148
|
+
await setDocumentOwner(req, data)
|
|
98
149
|
const id = await req.db[req.params.collection].create(data)
|
|
99
150
|
await util.notify('changed', req, req.params.collection, data)
|
|
100
151
|
return {
|
|
@@ -145,16 +196,23 @@ exports.updateById = async function (req) {
|
|
|
145
196
|
|
|
146
197
|
const doc = await req.db[req.params.collection].get(req.params.id)
|
|
147
198
|
const owner = doc.meta && doc.meta.owner
|
|
199
|
+
const ownerCollection = doc.meta && doc.meta.owner_collection
|
|
148
200
|
util.mongoUpdate(doc, modifier)
|
|
149
201
|
const newOwner = doc.meta && doc.meta.owner
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
const newOwnerCollection = doc.meta && doc.meta.owner_collection
|
|
203
|
+
if (owner !== newOwner || ownerCollection !== newOwnerCollection) {
|
|
204
|
+
if (req.hasPermission(`${req.params.collection}: modify owner`)) {
|
|
205
|
+
if (owner) {
|
|
206
|
+
await validateDocumentOwner(req, doc)
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
debug('attempting to change document owner.')
|
|
210
|
+
doc.meta.owner = owner
|
|
211
|
+
doc.meta.owner_collection = ownerCollection
|
|
212
|
+
}
|
|
153
213
|
}
|
|
154
214
|
req.body = doc
|
|
155
215
|
await util.notify('put', req, req.params.collection, doc)
|
|
156
|
-
|
|
157
|
-
req.body.meta.owner = newOwner
|
|
158
216
|
await req.db[req.params.collection].update(req.params.id, req.body)
|
|
159
217
|
await util.notify('changed', req, req.params.collection, req.body)
|
|
160
218
|
if (Object.keys(req.customResponseData)) {
|
package/controllers/users.js
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
const auth = require('../auth')
|
|
2
|
-
const util = require('../util')
|
|
3
|
-
const collectionsApi = require('./collections')
|
|
4
|
-
const permissions = require('../middleware/permissions')
|
|
5
|
-
|
|
6
|
-
exports.login = async (req, collection) => {
|
|
7
|
-
const password = req.body.password
|
|
8
|
-
|
|
9
|
-
if (typeof req.body.email !== 'string') {
|
|
10
|
-
throw new util.ApiError(400, 'email must be a string')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
req.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
const auth = require('../auth')
|
|
2
|
+
const util = require('../util')
|
|
3
|
+
const collectionsApi = require('./collections')
|
|
4
|
+
const permissions = require('../middleware/permissions')
|
|
5
|
+
|
|
6
|
+
exports.login = async (req, collection) => {
|
|
7
|
+
const password = req.body.password
|
|
8
|
+
|
|
9
|
+
if (typeof req.body.email !== 'string') {
|
|
10
|
+
throw new util.ApiError(400, 'email must be a string')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
req.body.email = req.body.email.toLowerCase()
|
|
14
|
+
|
|
15
|
+
// check if user exists
|
|
16
|
+
const result = await req.db[collection].find({
|
|
17
|
+
email: req.body.email
|
|
18
|
+
})
|
|
19
|
+
if (result.length === 0) {
|
|
20
|
+
throw new util.ApiError(400, 'No ' + collection + ' found with this email.')
|
|
21
|
+
}
|
|
22
|
+
const user = result[0]
|
|
23
|
+
if (!auth.isValidPassword(password, user.password)) {
|
|
24
|
+
throw new util.ApiError(401, 'Incorrect password')
|
|
25
|
+
}
|
|
26
|
+
const jwt_options = req.settings.jwt_expires_in ? { expiresIn: req.settings.jwt_expires_in } : {}
|
|
27
|
+
const payload = auth.doLogin({
|
|
28
|
+
id: user._id,
|
|
29
|
+
collection,
|
|
30
|
+
timestamp: user.meta.password_last_updated_at,
|
|
31
|
+
jwt_secret: req.getSetting('jwt_secret'),
|
|
32
|
+
jwt_options
|
|
33
|
+
})
|
|
34
|
+
req.uid = user._id
|
|
35
|
+
req.ucollection = collection
|
|
36
|
+
req.user = user
|
|
37
|
+
await permissions.addRolePermissionsAsync(req)
|
|
38
|
+
payload.canUseAdmin = req.hasPermission('login to admin')
|
|
39
|
+
return payload
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.register = function (req, collection) {
|
|
43
|
+
req.url = `/${collection}`
|
|
44
|
+
req.params.collection = collection
|
|
45
|
+
return collectionsApi.insert(req)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
exports.getMe = function (req, collection) {
|
|
49
|
+
req.params.collection = collection
|
|
50
|
+
req.params.id = req.uid
|
|
51
|
+
return collectionsApi.getById(req)
|
|
52
|
+
}
|
package/listeners.js
CHANGED
|
@@ -18,19 +18,11 @@ module.exports = function (api) {
|
|
|
18
18
|
Object.assign(req.settings, data)
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
api.addListener(['put', 'post'], function updateMetadata (req, collection, data, { event }) {
|
|
22
|
-
data.meta
|
|
21
|
+
api.addListener(['put', 'post'], async function updateMetadata (req, collection, data, { event }) {
|
|
22
|
+
data.meta ??= {}
|
|
23
23
|
data.meta.updated = new Date().toISOString()
|
|
24
24
|
if (event === 'post') {
|
|
25
25
|
data.meta.created = new Date().toISOString()
|
|
26
|
-
if (req.user) {
|
|
27
|
-
if (req.hasPermission('login to admin')) {
|
|
28
|
-
data.meta.owner = data.meta.owner || req.user._id
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
data.meta.owner = req.user._id
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
26
|
}
|
|
35
27
|
})
|
|
36
28
|
|
|
@@ -63,6 +55,9 @@ module.exports = function (api) {
|
|
|
63
55
|
class: 'comment-link'
|
|
64
56
|
}
|
|
65
57
|
]
|
|
58
|
+
},
|
|
59
|
+
schema.properties.meta.properties.owner_collection = {
|
|
60
|
+
type: 'string'
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
}
|
|
@@ -19,11 +19,8 @@ module.exports = function (api) {
|
|
|
19
19
|
|
|
20
20
|
api.addListener(['get', 'put', 'post', 'delete'], function collectionPermissionCheck (req, collection, data, info) {
|
|
21
21
|
const permission = eventToPermissionMapping[info.event]
|
|
22
|
-
const editingSelf = collection === req.ucollection && data._id === req.uid
|
|
23
22
|
const editingOwnDoc = data.meta && data.meta.owner && data.meta.owner === req.uid
|
|
24
|
-
const editingOwn = (
|
|
25
|
-
req.hasPermission(collection + ': ' + permission + ' own'))
|
|
26
|
-
// console.log(editingOwn + ' ' + editingSelf + ' ' + editingOwnDoc);
|
|
23
|
+
const editingOwn = editingOwnDoc && req.hasPermission(collection + ': ' + permission + ' own')
|
|
27
24
|
if (!editingOwn && !req.hasPermission(collection + ': ' + permission)) {
|
|
28
25
|
debug(`cancelling, missing permission "${collection}: ${permission}"`)
|
|
29
26
|
throw new util.ApiError(401, 'You do not have permission to perform this action.')
|
package/listeners_users.js
CHANGED
|
@@ -1,88 +1,92 @@
|
|
|
1
|
-
const auth = require('./auth')
|
|
2
|
-
const util = require('./util.js')
|
|
3
|
-
const debug = require('debug')('expressa')
|
|
4
|
-
|
|
5
|
-
module.exports = async function(api) {
|
|
6
|
-
|
|
7
|
-
const loginCollections = (await util.getLoginCollections(api)).map((coll) => coll._id)
|
|
8
|
-
|
|
9
|
-
api.addCollectionListener(['post', 'put'], loginCollections, async function updatePassword(req, collection, data) {
|
|
10
|
-
if (req.method === 'PUT') {
|
|
11
|
-
if (!data.password) {
|
|
12
|
-
const oldData = await api.db[collection].get(data._id)
|
|
13
|
-
data.password = oldData.password // preserve password if not explicitly set
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
if (data.password && data.password.length !== 60 && data.password[0] !== '$') {
|
|
17
|
-
debug('hashing and replacing password in the user document.')
|
|
18
|
-
data.password = auth.createHash(data.password)
|
|
19
|
-
data.meta.password_last_updated_at = new Date().toISOString()
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
api.addCollectionListener('post', loginCollections, async function roleCreateCheck(req, collection, data) {
|
|
24
|
-
if (!data.roles) {
|
|
25
|
-
data.roles = []
|
|
26
|
-
}
|
|
27
|
-
// user special case to allow first user to be admin
|
|
28
|
-
if (collection === 'users') {
|
|
29
|
-
const hasUsers = (await api.db.users.find({}, 0, 1)).length === 1
|
|
30
|
-
if (!hasUsers && !data.roles.includes('Admin')) {
|
|
31
|
-
throw new util.ApiError(400, 'first user must have role Admin')
|
|
32
|
-
}
|
|
33
|
-
if (hasUsers && data.roles.length > 0 && !req.hasPermission('users: modify roles')) {
|
|
34
|
-
throw new util.ApiError(400, 'insufficient permissions to create user with roles')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
if (data.roles.length > 0 && !req.hasPermission(`${collection}: modify roles`)) {
|
|
39
|
-
throw new util.ApiError(400, `insufficient permissions to create ${collection} with roles`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
api.addLateCollectionListener('put', loginCollections, async function roleChangeCheck(req, collection, data) {
|
|
45
|
-
if (!req.hasPermission(`${collection}: modify roles`)) {
|
|
46
|
-
const oldData = await api.db[collection].get(data._id)
|
|
47
|
-
data.roles = oldData.roles
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
api.addCollectionListenerWithPriority(['get', 'changed', 'deleted'], loginCollections, 100, function hidePasswordHashes(req, collection, data) {
|
|
52
|
-
if (!req.hasPermission(`${collection}: view hashed passwords`)) {
|
|
53
|
-
debug(`deleting password because "${collection}: view hashed passwords"-permission is not set`)
|
|
54
|
-
delete data.password
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
api.addCollectionListener('post', loginCollections, async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
const auth = require('./auth')
|
|
2
|
+
const util = require('./util.js')
|
|
3
|
+
const debug = require('debug')('expressa')
|
|
4
|
+
|
|
5
|
+
module.exports = async function(api) {
|
|
6
|
+
|
|
7
|
+
const loginCollections = (await util.getLoginCollections(api)).map((coll) => coll._id)
|
|
8
|
+
|
|
9
|
+
api.addCollectionListener(['post', 'put'], loginCollections, async function updatePassword(req, collection, data) {
|
|
10
|
+
if (req.method === 'PUT') {
|
|
11
|
+
if (!data.password) {
|
|
12
|
+
const oldData = await api.db[collection].get(data._id)
|
|
13
|
+
data.password = oldData.password // preserve password if not explicitly set
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (data.password && data.password.length !== 60 && data.password[0] !== '$') {
|
|
17
|
+
debug('hashing and replacing password in the user document.')
|
|
18
|
+
data.password = auth.createHash(data.password)
|
|
19
|
+
data.meta.password_last_updated_at = new Date().toISOString()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
api.addCollectionListener('post', loginCollections, async function roleCreateCheck(req, collection, data) {
|
|
24
|
+
if (!data.roles) {
|
|
25
|
+
data.roles = []
|
|
26
|
+
}
|
|
27
|
+
// user special case to allow first user to be admin
|
|
28
|
+
if (collection === 'users') {
|
|
29
|
+
const hasUsers = (await api.db.users.find({}, 0, 1)).length === 1
|
|
30
|
+
if (!hasUsers && !data.roles.includes('Admin')) {
|
|
31
|
+
throw new util.ApiError(400, 'first user must have role Admin')
|
|
32
|
+
}
|
|
33
|
+
if (hasUsers && data.roles.length > 0 && !req.hasPermission('users: modify roles')) {
|
|
34
|
+
throw new util.ApiError(400, 'insufficient permissions to create user with roles')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (data.roles.length > 0 && !req.hasPermission(`${collection}: modify roles`)) {
|
|
39
|
+
throw new util.ApiError(400, `insufficient permissions to create ${collection} with roles`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
api.addLateCollectionListener('put', loginCollections, async function roleChangeCheck(req, collection, data) {
|
|
45
|
+
if (!req.hasPermission(`${collection}: modify roles`)) {
|
|
46
|
+
const oldData = await api.db[collection].get(data._id)
|
|
47
|
+
data.roles = oldData.roles
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
api.addCollectionListenerWithPriority(['get', 'changed', 'deleted'], loginCollections, 100, function hidePasswordHashes(req, collection, data) {
|
|
52
|
+
if (!req.hasPermission(`${collection}: view hashed passwords`)) {
|
|
53
|
+
debug(`deleting password because "${collection}: view hashed passwords"-permission is not set`)
|
|
54
|
+
delete data.password
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
api.addCollectionListener(['post', 'put'], loginCollections, async function userEmailLowerCase(req, collection, data) {
|
|
59
|
+
data.email = data.email.toLowerCase()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
api.addCollectionListener('post', loginCollections, async function userUniquenessCheck(req, collection, data) {
|
|
63
|
+
const result = await api.db[collection].find({email: data.email})
|
|
64
|
+
if (result.length > 0) {
|
|
65
|
+
throw new util.ApiError(409, 'This email is already registered.')
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/* Mantain users-roles reference */
|
|
70
|
+
api.addCollectionListener('changed', 'role', async function addRoleToUserSchema(req, collection, data) {
|
|
71
|
+
for (const coll of loginCollections) {
|
|
72
|
+
const doc = await api.db.collection.get(coll)
|
|
73
|
+
if (!doc.schema.properties.roles.items.enum.includes(data._id)) {
|
|
74
|
+
doc.schema.properties.roles.items.enum.push(data._id)
|
|
75
|
+
await api.db.collection.update(coll, doc)
|
|
76
|
+
await api.notify('changed', req, 'collection', doc)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
api.addCollectionListener('deleted', 'role', async function removeRoleToUserSchema(req, collection, data) {
|
|
82
|
+
for (const coll of loginCollections) {
|
|
83
|
+
const doc = await api.db.collection.get(coll)
|
|
84
|
+
const roles = doc.schema.properties.roles.items.enum
|
|
85
|
+
if (roles.includes(data._id)) {
|
|
86
|
+
roles.splice(roles.indexOf(data._id), 1)
|
|
87
|
+
await api.db.collection.update(coll, doc)
|
|
88
|
+
await api.notify('changed', req, 'collection', doc)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
package/listeners_validation.js
CHANGED
|
@@ -1,78 +1,43 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type: 'string'
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Load all validators
|
|
46
|
-
const collections = await api.db.collection.all()
|
|
47
|
-
collections.forEach((collection) => {
|
|
48
|
-
const schema = addImplicitFields(collection.schema)
|
|
49
|
-
schemaValidators[collection._id] = ajv.compile(schema)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
// Load new and update validators as necessary
|
|
53
|
-
api.addCollectionListener('changed', 'collection', function updateSchemaValidators (req, collection, data) {
|
|
54
|
-
const schema = addImplicitFields(data.schema)
|
|
55
|
-
schemaValidators[data._id] = ajv.compile(schema)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
api.addCollectionListener('get', ['collection', 'schemas'], function ensureIdAdded (req, collection, data) {
|
|
59
|
-
if (data.schema) {
|
|
60
|
-
data.schema = addImplicitFields(data.schema)
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
api.addListener(['put', 'post'], function matchesSchema (req, collection, data) {
|
|
65
|
-
// TODO (switch to check if "installed" after install tests are done.
|
|
66
|
-
if (!req.settings.enforce_permissions) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
if (!schemaValidators[collection]) {
|
|
70
|
-
console.error(`missing schema validator for collection ${collection}`)
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
const isValid = schemaValidators[collection](data)
|
|
74
|
-
if (!isValid) {
|
|
75
|
-
throw new util.ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
}
|
|
1
|
+
const util = require('./util')
|
|
2
|
+
|
|
3
|
+
function addImplicitFields (schema) {
|
|
4
|
+
schema['properties'] ??= {}
|
|
5
|
+
schema['properties']['meta'] ??= { type: 'object' }
|
|
6
|
+
schema['properties']['meta']['properties'] ??= {}
|
|
7
|
+
schema['properties']['meta']['properties']['created'] ??= { type: 'string' }
|
|
8
|
+
schema['properties']['meta']['properties']['updated'] ??= { type: 'string' }
|
|
9
|
+
schema['properties']['meta']['properties']['owner'] ??= { type: 'string' }
|
|
10
|
+
schema['properties']['meta']['properties']['owner_collection'] ??= { type: 'string' }
|
|
11
|
+
schema['properties']['_id'] ??= { type: 'string' }
|
|
12
|
+
return schema
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = async function (api) {
|
|
16
|
+
|
|
17
|
+
// Load all validators
|
|
18
|
+
const collections = await api.db.collection.all()
|
|
19
|
+
collections.forEach((collection) => {
|
|
20
|
+
const schema = addImplicitFields(collection.schema)
|
|
21
|
+
util.addSchemaValidator(collection._id, schema)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Load new and update validators as necessary
|
|
25
|
+
api.addCollectionListener('changed', 'collection', function updateSchemaValidators (req, collection, data) {
|
|
26
|
+
const schema = addImplicitFields(data.schema)
|
|
27
|
+
util.addSchemaValidator(data._id, schema)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
api.addCollectionListener('get', ['collection', 'schemas'], function ensureIdAdded (req, collection, data) {
|
|
31
|
+
if (data.schema) {
|
|
32
|
+
data.schema = addImplicitFields(data.schema)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
api.addListener(['put', 'post'], function matchesSchema (req, collection, data) {
|
|
37
|
+
// TODO (switch to check if "installed" after install tests are done.
|
|
38
|
+
if (!req.settings.enforce_permissions) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
util.validateSchema(collection, data)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
function getCollectionPermissions (name, hasOwner) {
|
|
2
|
-
let permissions =
|
|
2
|
+
let permissions = exports.collectionPermissions(name)
|
|
3
3
|
if (hasOwner) {
|
|
4
|
-
permissions = permissions.concat(
|
|
4
|
+
permissions = permissions.concat(exports.collectionOwnerPermissions(name))
|
|
5
5
|
}
|
|
6
6
|
return permissions
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
exports.collectionPermissions = function (name) {
|
|
10
|
+
return ['create', 'view', 'edit', 'modify owner', 'delete'].map((action) => `${name}: ${action}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.collectionOwnerPermissions = function (name) {
|
|
14
|
+
return ['view', 'edit', 'delete'].map((action) => `${name}: ${action} own`)
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
exports.permissions = async function (app) {
|
|
10
18
|
const collections = (await app.db.collection.all()).map((collection) => collection._id)
|
|
11
19
|
// flatten the permissions into a single list
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { collectionPermissions, collectionOwnerPermissions } = require('../collections/collections')
|
|
2
|
+
|
|
1
3
|
exports.settingSchema = {
|
|
2
4
|
enforce_permissions: {
|
|
3
5
|
type: 'boolean',
|
|
@@ -57,14 +59,6 @@ exports.install = async function (app) {
|
|
|
57
59
|
|
|
58
60
|
exports.permissions = ['users: modify roles', 'login to admin']
|
|
59
61
|
|
|
60
|
-
function collectionPermissions (name) {
|
|
61
|
-
return ['create', 'view', 'edit', 'delete'].map((action) => `${name}: ${action}`)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function collectionOwnerPermissions (name) {
|
|
65
|
-
return ['view', 'edit', 'delete'].map((action) => `${name}: ${action} own`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
exports.init = async function (api) {
|
|
69
63
|
api.addCollectionListener(['changed'], 'collection', async function addCollectionPerms (req, collection, data) {
|
|
70
64
|
if (api.db.role) {
|
package/package.json
CHANGED
package/test/0-install.js
CHANGED
|
@@ -63,6 +63,17 @@ describe('install flow', function () {
|
|
|
63
63
|
expect(u.roles || []).to.include('Admin')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
it('can login admin account', async function() {
|
|
67
|
+
const res = await request(app)
|
|
68
|
+
.post('/users/login')
|
|
69
|
+
.send({
|
|
70
|
+
email: 'a@example.com',
|
|
71
|
+
password: '123'
|
|
72
|
+
})
|
|
73
|
+
.expect(200)
|
|
74
|
+
testutils.setAdminToken(res.body.token)
|
|
75
|
+
})
|
|
76
|
+
|
|
66
77
|
it('2nd account cannot be admin account', async function() {
|
|
67
78
|
await request(app)
|
|
68
79
|
.post('/users/register')
|
|
@@ -11,7 +11,7 @@ const { app, api } = testutils
|
|
|
11
11
|
*/
|
|
12
12
|
describe('querying collections', function () {
|
|
13
13
|
it('create doc 1', async function () {
|
|
14
|
-
const token = await
|
|
14
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: create')
|
|
15
15
|
await request(app)
|
|
16
16
|
.post('/testdoc')
|
|
17
17
|
.set('x-access-token', token)
|
|
@@ -28,7 +28,7 @@ describe('querying collections', function () {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it('create doc 2', async function () {
|
|
31
|
-
const token = await
|
|
31
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: create')
|
|
32
32
|
await request(app)
|
|
33
33
|
.post('/testdoc')
|
|
34
34
|
.set('x-access-token', token)
|
|
@@ -42,7 +42,7 @@ describe('querying collections', function () {
|
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
it('create doc 3', async function () {
|
|
45
|
-
const token = await
|
|
45
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: create')
|
|
46
46
|
await request(app)
|
|
47
47
|
.post('/testdoc')
|
|
48
48
|
.set('x-access-token', token)
|
|
@@ -60,7 +60,7 @@ describe('querying collections', function () {
|
|
|
60
60
|
let token
|
|
61
61
|
|
|
62
62
|
it('read doc by id', async function () {
|
|
63
|
-
token = await
|
|
63
|
+
token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
64
64
|
const res = await request(app)
|
|
65
65
|
.get('/testdoc/testid123')
|
|
66
66
|
.set('x-access-token', token)
|
|
@@ -202,7 +202,7 @@ describe('querying collections', function () {
|
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
it('sort by deep field ascending', async function () {
|
|
205
|
-
const token = await
|
|
205
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
206
206
|
const res = await request(app)
|
|
207
207
|
.get('/testdoc?orderby={"data.number":1}')
|
|
208
208
|
.set('x-access-token', token)
|
|
@@ -214,7 +214,7 @@ describe('querying collections', function () {
|
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
it('sort by deep field descending', async function () {
|
|
217
|
-
const token = await
|
|
217
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
218
218
|
const res = await request(app)
|
|
219
219
|
.get('/testdoc?orderby={"meta.created":-1}')
|
|
220
220
|
.set('x-access-token', token)
|
|
@@ -226,7 +226,7 @@ describe('querying collections', function () {
|
|
|
226
226
|
})
|
|
227
227
|
|
|
228
228
|
it('project a specific field', async function () {
|
|
229
|
-
const token = await
|
|
229
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
230
230
|
const res = await request(app)
|
|
231
231
|
.get('/testdoc?fields={"title":1}')
|
|
232
232
|
.set('x-access-token', token)
|
|
@@ -237,7 +237,7 @@ describe('querying collections', function () {
|
|
|
237
237
|
})
|
|
238
238
|
|
|
239
239
|
it('project a specific field on get', async function () {
|
|
240
|
-
const token = await
|
|
240
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
241
241
|
const res = await request(app)
|
|
242
242
|
.get('/testdoc/test1?fields={"title":1}')
|
|
243
243
|
.set('x-access-token', token)
|
|
@@ -247,7 +247,7 @@ describe('querying collections', function () {
|
|
|
247
247
|
})
|
|
248
248
|
|
|
249
249
|
it('project deep fields', async function() {
|
|
250
|
-
const token = await
|
|
250
|
+
const token = await testutils.getUserWithPermissions(api, 'testdoc: view')
|
|
251
251
|
const res = await request(app)
|
|
252
252
|
.get('/testdoc?fields={"data":1}')
|
|
253
253
|
.set('x-access-token', token)
|
|
@@ -266,4 +266,4 @@ describe('querying collections', function () {
|
|
|
266
266
|
expect(res2.body[0].data.number).to.equal(0)
|
|
267
267
|
expect(res2.body[0].data.field).to.be.undefined
|
|
268
268
|
})
|
|
269
|
-
})
|
|
269
|
+
})
|
package/test/test.js
CHANGED
|
@@ -70,7 +70,7 @@ describe('General Tests:', () => {
|
|
|
70
70
|
res.send(req.user)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
const token = await
|
|
73
|
+
const token = await testutils.getUserWithPermissions(api, [])
|
|
74
74
|
const res = await request(app)
|
|
75
75
|
.get('/test')
|
|
76
76
|
.set('x-access-token', token)
|
package/test/testutils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
|
|
3
3
|
const expressa = require('../')
|
|
4
|
+
const request = require('supertest')
|
|
4
5
|
const util = require('../util')
|
|
5
6
|
|
|
6
7
|
// See https://stackoverflow.com/questions/18052762/remove-directory-which-is-not-empty
|
|
@@ -25,11 +26,62 @@ exports.api = expressa.api({
|
|
|
25
26
|
file_storage_path: 'testdata'
|
|
26
27
|
})
|
|
27
28
|
const express = require('express')
|
|
29
|
+
const randomstring = require('randomstring')
|
|
28
30
|
exports.app = express()
|
|
29
31
|
exports.app.use(exports.api)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const tokens = {
|
|
34
|
+
admin: ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.setAdminToken = function(token) {
|
|
38
|
+
tokens.admin = token
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
exports.getUserWithPermissions = async function(api, permissions) {
|
|
42
|
+
const service = request(exports.app)
|
|
43
|
+
if (typeof permissions === 'string') {
|
|
44
|
+
permissions = [permissions]
|
|
45
|
+
}
|
|
46
|
+
permissions = permissions || []
|
|
47
|
+
const permissionsMap = {}
|
|
48
|
+
permissions.forEach(function (permission) {
|
|
49
|
+
permissionsMap[permission] = true
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const randId = randomstring.generate(12)
|
|
53
|
+
const roleName = 'role' + randId
|
|
54
|
+
const role = {
|
|
55
|
+
_id: roleName,
|
|
56
|
+
permissions: permissionsMap
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const roleRes = await service.post('/role')
|
|
60
|
+
.set('x-access-token', tokens.admin)
|
|
61
|
+
.send(role)
|
|
62
|
+
.expect(200)
|
|
63
|
+
|
|
64
|
+
const user = {
|
|
65
|
+
email: 'test' + randId + '@example.com',
|
|
66
|
+
password: '123',
|
|
67
|
+
}
|
|
68
|
+
const registerRes = await service.post('/users/register')
|
|
69
|
+
.send(user)
|
|
70
|
+
.expect(200)
|
|
71
|
+
|
|
72
|
+
const updateRes = await service.post(`/users/${registerRes.body.id}/update`)
|
|
73
|
+
.send({ $push: { roles: roleName } })
|
|
74
|
+
.set('x-access-token', tokens.admin)
|
|
75
|
+
.expect(200)
|
|
76
|
+
|
|
77
|
+
const loginRes = await service.post('/users/login')
|
|
78
|
+
.send(user)
|
|
79
|
+
.expect(200)
|
|
80
|
+
|
|
81
|
+
user._id = loginRes.body.uid
|
|
82
|
+
return loginRes.body.token
|
|
83
|
+
}
|
|
32
84
|
exports.clone = util.clone
|
|
33
85
|
exports.sleep = function sleep(ms) {
|
|
34
86
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
35
|
-
}
|
|
87
|
+
}
|
package/test/users.js
CHANGED
|
@@ -189,4 +189,123 @@ describe('user functionality', function () {
|
|
|
189
189
|
.expect(200)
|
|
190
190
|
expect(res3.body.properties.roles.items.enum).to.not.include('testrole')
|
|
191
191
|
})
|
|
192
|
+
|
|
193
|
+
it('cannot change owner by default', async function () {
|
|
194
|
+
const token = await testutils.getUserWithPermissions(api, ['users: view', 'users: edit'])
|
|
195
|
+
|
|
196
|
+
const res = await request(app)
|
|
197
|
+
.get(`/users/me`)
|
|
198
|
+
.set('x-access-token', token)
|
|
199
|
+
const newUser = res.body
|
|
200
|
+
|
|
201
|
+
await request(app)
|
|
202
|
+
.post(`/users/${newUser._id}/update`)
|
|
203
|
+
.set('x-access-token', token)
|
|
204
|
+
.send({ $set: { 'meta.owner': user._id } }) // try change owner to another user
|
|
205
|
+
.expect(200) // will succeed but owner will not change
|
|
206
|
+
|
|
207
|
+
const res2 = await request(app)
|
|
208
|
+
.get(`/users/${newUser._id}`)
|
|
209
|
+
.set('x-access-token', token)
|
|
210
|
+
expect(res2.body.meta.owner).to.not.equal(user._id)
|
|
211
|
+
expect(res2.body.meta.owner).to.equal(newUser._id)
|
|
212
|
+
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('can change owner with correct permission', async function () {
|
|
216
|
+
const token = await testutils.getUserWithPermissions(api, ['users: view', 'users: edit', 'users: modify owner'])
|
|
217
|
+
|
|
218
|
+
const res = await request(app)
|
|
219
|
+
.get(`/users/me`)
|
|
220
|
+
.set('x-access-token', token)
|
|
221
|
+
const newUser = res.body
|
|
222
|
+
|
|
223
|
+
await request(app)
|
|
224
|
+
.post(`/users/${newUser._id}/update`)
|
|
225
|
+
.set('x-access-token', token)
|
|
226
|
+
.send({ $set: { 'meta.owner': user._id } }) // try change owner to another user
|
|
227
|
+
.expect(200)
|
|
228
|
+
|
|
229
|
+
const res2 = await request(app)
|
|
230
|
+
.get(`/users/${newUser._id}`)
|
|
231
|
+
.set('x-access-token', token)
|
|
232
|
+
expect(res2.body.meta.owner).to.equal(user._id)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('changing to bogus owner fails', async function () {
|
|
236
|
+
const token = await testutils.getUserWithPermissions(api, ['users: view', 'users: edit', 'users: modify owner'])
|
|
237
|
+
|
|
238
|
+
const res = await request(app)
|
|
239
|
+
.get(`/users/me`)
|
|
240
|
+
.set('x-access-token', token)
|
|
241
|
+
const newUser = res.body
|
|
242
|
+
|
|
243
|
+
await request(app)
|
|
244
|
+
.post(`/users/${newUser._id}/update`)
|
|
245
|
+
.set('x-access-token', token)
|
|
246
|
+
.send({ $set: { 'meta.owner': 'owner-that-doesnt-exist' } }) // try change owner to another user
|
|
247
|
+
.expect(417)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('cannot set owner on creation by default', async function () {
|
|
251
|
+
const token = await testutils.getUserWithPermissions(api, ['users: view', 'users: edit', 'testdoc: create', 'testdoc: view own'])
|
|
252
|
+
|
|
253
|
+
const res = await request(app)
|
|
254
|
+
.get(`/users/me`)
|
|
255
|
+
.set('x-access-token', token)
|
|
256
|
+
const newUser = res.body
|
|
257
|
+
|
|
258
|
+
const doc = {
|
|
259
|
+
title: 'Test Title',
|
|
260
|
+
meta: {
|
|
261
|
+
owner: user._id,
|
|
262
|
+
owner_collection: 'users',
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const res2 = await request(app)
|
|
267
|
+
.post(`/testdoc`)
|
|
268
|
+
.set('x-access-token', token)
|
|
269
|
+
.send(doc)
|
|
270
|
+
.expect(200) // will succeed but owner will not change
|
|
271
|
+
doc._id = res2.body.id
|
|
272
|
+
|
|
273
|
+
const res3 = await request(app)
|
|
274
|
+
.get(`/testdoc/${doc._id}`)
|
|
275
|
+
.set('x-access-token', token)
|
|
276
|
+
.expect(200)
|
|
277
|
+
expect(res3.body.meta.owner).to.not.equal(user._id)
|
|
278
|
+
expect(res3.body.meta.owner).to.equal(newUser._id)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('can set owner on creation with correct permission', async function () {
|
|
282
|
+
const token = await testutils.getUserWithPermissions(api, ['users: view', 'users: edit', 'testdoc: create', 'testdoc: view', 'testdoc: modify owner'])
|
|
283
|
+
|
|
284
|
+
const res = await request(app)
|
|
285
|
+
.get(`/users/me`)
|
|
286
|
+
.set('x-access-token', token)
|
|
287
|
+
const newUser = res.body
|
|
288
|
+
|
|
289
|
+
const doc = {
|
|
290
|
+
title: 'Test Title',
|
|
291
|
+
meta: {
|
|
292
|
+
owner: user._id,
|
|
293
|
+
owner_collection: 'users',
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const res2 = await request(app)
|
|
298
|
+
.post(`/testdoc`)
|
|
299
|
+
.set('x-access-token', token)
|
|
300
|
+
.send(doc)
|
|
301
|
+
.expect(200) // will succeed but owner will not change
|
|
302
|
+
doc._id = res2.body.id
|
|
303
|
+
|
|
304
|
+
const res3 = await request(app)
|
|
305
|
+
.get(`/testdoc/${doc._id}`)
|
|
306
|
+
.set('x-access-token', token)
|
|
307
|
+
.expect(200)
|
|
308
|
+
expect(res3.body.meta.owner).to.not.equal(newUser._id)
|
|
309
|
+
expect(res3.body.meta.owner).to.equal(user._id)
|
|
310
|
+
})
|
|
192
311
|
})
|
package/util.js
CHANGED
|
@@ -6,6 +6,48 @@ const pg = require('pg')
|
|
|
6
6
|
const pgPools = {}
|
|
7
7
|
const dot = require('dot-object')
|
|
8
8
|
const mongoQuery = require('mongo-query')
|
|
9
|
+
const Ajv = require('ajv')
|
|
10
|
+
const ajv = new Ajv({
|
|
11
|
+
allErrors: true,
|
|
12
|
+
strict: 'log',
|
|
13
|
+
strictSchema: 'log',
|
|
14
|
+
validateFormats: false,
|
|
15
|
+
allowUnionTypes: true,
|
|
16
|
+
})
|
|
17
|
+
ajv.addKeyword({
|
|
18
|
+
keyword: 'links',
|
|
19
|
+
type: 'string',
|
|
20
|
+
schemaType: 'array',
|
|
21
|
+
})
|
|
22
|
+
ajv.addKeyword({
|
|
23
|
+
keyword: 'media',
|
|
24
|
+
type: 'string',
|
|
25
|
+
schemaType: 'object',
|
|
26
|
+
})
|
|
27
|
+
ajv.addKeyword({
|
|
28
|
+
keyword: 'propertyOrder',
|
|
29
|
+
schemaType: 'number',
|
|
30
|
+
})
|
|
31
|
+
const formatKey = ajv.getKeyword('format')
|
|
32
|
+
formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
|
|
33
|
+
|
|
34
|
+
const schemaValidators = {}
|
|
35
|
+
|
|
36
|
+
exports.addSchemaValidator = function(collection, schema) {
|
|
37
|
+
schemaValidators[collection] = ajv.compile(schema)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.validateSchema = function(collection, doc) {
|
|
41
|
+
if (!schemaValidators[collection]) {
|
|
42
|
+
console.error(`missing schema validator for collection ${collection}`)
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
const isValid = schemaValidators[collection](doc)
|
|
46
|
+
if (!isValid) {
|
|
47
|
+
throw new ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
9
51
|
|
|
10
52
|
exports.orderBy = function (data, orderby) {
|
|
11
53
|
data.sort(function compare (a, b) {
|
|
@@ -160,44 +202,6 @@ exports.createSecureRandomId = function() {
|
|
|
160
202
|
return crypto.randomBytes(24).toString('hex')
|
|
161
203
|
}
|
|
162
204
|
|
|
163
|
-
exports.getUserWithPermissions = async function (api, permissions) {
|
|
164
|
-
if (typeof permissions === 'string') {
|
|
165
|
-
permissions = [permissions]
|
|
166
|
-
}
|
|
167
|
-
permissions = permissions || []
|
|
168
|
-
const permissionsMap = {}
|
|
169
|
-
permissions.forEach(function (permission) {
|
|
170
|
-
permissionsMap[permission] = true
|
|
171
|
-
})
|
|
172
|
-
const randId = randomstring.generate(12)
|
|
173
|
-
const roleName = 'role' + randId
|
|
174
|
-
const now = new Date().toISOString()
|
|
175
|
-
const user = {
|
|
176
|
-
email: 'test' + randId + '@example.com',
|
|
177
|
-
password: '123',
|
|
178
|
-
collection: 'users',
|
|
179
|
-
roles: [roleName],
|
|
180
|
-
meta: {
|
|
181
|
-
created: now,
|
|
182
|
-
updated: now,
|
|
183
|
-
password_last_updated_at: now,
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
await api.db.role.cache.create({
|
|
187
|
-
_id: roleName,
|
|
188
|
-
permissions: permissionsMap
|
|
189
|
-
})
|
|
190
|
-
const result = await api.db.users.create(user)
|
|
191
|
-
user._id = result
|
|
192
|
-
const payload = api.util.doLogin({
|
|
193
|
-
id: user._id,
|
|
194
|
-
collection: 'users',
|
|
195
|
-
timestamp: user.meta.password_last_updated_at,
|
|
196
|
-
jwt_secret: api.settings.jwt_secret,
|
|
197
|
-
})
|
|
198
|
-
return payload.token
|
|
199
|
-
}
|
|
200
|
-
|
|
201
205
|
const severities = ['critical', 'error', 'warning', 'notice', 'info', 'debug']
|
|
202
206
|
exports.getLogSeverity = function (status) {
|
|
203
207
|
const severity = status >= 500 ? 'error'
|
|
@@ -339,7 +343,7 @@ exports.addIdIfMissing = function addIdIfMissing (document) {
|
|
|
339
343
|
}
|
|
340
344
|
|
|
341
345
|
exports.sortObjectKeys = function sortObjectKeys(object) {
|
|
342
|
-
if (typeof object != 'object' || object instanceof Array) { // Do not sort the array
|
|
346
|
+
if (typeof object != 'object' || object instanceof Array || !object) { // Do not sort the array
|
|
343
347
|
return object
|
|
344
348
|
}
|
|
345
349
|
const keys = Object.keys(object)
|
|
@@ -364,10 +368,10 @@ exports.getLoginCollections = async function(api) {
|
|
|
364
368
|
function isValidLoginCollection(collection) {
|
|
365
369
|
const name = collection._id
|
|
366
370
|
if(collection.enableLogin === true) {
|
|
367
|
-
const properties = collection.schema
|
|
368
|
-
if (properties && properties.
|
|
369
|
-
const required = collection.schema
|
|
370
|
-
if (required
|
|
371
|
+
const properties = collection.schema?.properties
|
|
372
|
+
if (properties && properties.password && properties.roles) {
|
|
373
|
+
const required = collection.schema?.required
|
|
374
|
+
if (required?.includes('password')) {
|
|
371
375
|
return true
|
|
372
376
|
}
|
|
373
377
|
else {
|