expressa 2.0.6 → 2.0.8
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/auth/index.js +50 -47
- package/db/file.js +2 -4
- package/db/memory.js +2 -4
- package/db/postgres.js +93 -100
- package/index.js +0 -2
- package/listeners_users.js +1 -1
- package/listeners_validation.js +2 -2
- package/modules/admin/package.json +1 -1
- package/package.json +1 -1
- package/util.js +36 -2
package/auth/index.js
CHANGED
|
@@ -1,47 +1,50 @@
|
|
|
1
|
-
const bcrypt = require('bcryptjs')
|
|
2
|
-
const handler = require('./jwt')
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
req.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
const bcrypt = require('bcryptjs')
|
|
2
|
+
const handler = require('./jwt')
|
|
3
|
+
|
|
4
|
+
exports.isHashed = function (string) {
|
|
5
|
+
return string && string.length === 60 && string[0] === '$'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
exports.createHash = function (password) {
|
|
9
|
+
const salt = bcrypt.genSaltSync(10)
|
|
10
|
+
return bcrypt.hashSync(password, salt)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.isValidPassword = function (password, hashedPassword) {
|
|
14
|
+
return bcrypt.compareSync(password, hashedPassword)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.doLogin = handler.doLogin
|
|
18
|
+
|
|
19
|
+
exports.middleware = async function authMiddleware(req, res, next) {
|
|
20
|
+
req.query = req.query || {}
|
|
21
|
+
const token = req.query['token'] || req.headers['x-access-token']
|
|
22
|
+
delete req.query['token']
|
|
23
|
+
delete req.headers['x-access-token']
|
|
24
|
+
let payload
|
|
25
|
+
try {
|
|
26
|
+
payload = await handler.isLoggedIn(token, req.getSetting('jwt_secret'))
|
|
27
|
+
}
|
|
28
|
+
catch(e) {
|
|
29
|
+
req.uerror = e.message
|
|
30
|
+
}
|
|
31
|
+
if (payload) {
|
|
32
|
+
let user
|
|
33
|
+
try {
|
|
34
|
+
user = await req.db[payload.collection].get(payload._id)
|
|
35
|
+
} catch (e) {
|
|
36
|
+
req.uerror = 'user no longer exists'
|
|
37
|
+
}
|
|
38
|
+
if (user) {
|
|
39
|
+
if (req.getSetting('jwt_expire_on_password_change') && user.meta.password_last_updated_at !== payload.timestamp) {
|
|
40
|
+
req.uerror = 'jwt expired'
|
|
41
|
+
}
|
|
42
|
+
if (!req.uerror) {
|
|
43
|
+
req.uid = payload._id
|
|
44
|
+
req.ucollection = payload.collection
|
|
45
|
+
req.user = user
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
next()
|
|
50
|
+
}
|
package/db/file.js
CHANGED
|
@@ -5,8 +5,6 @@ Store.prototype.allAsync = promisify(Store.prototype.all)
|
|
|
5
5
|
Store.prototype.getAsync = promisify(Store.prototype.get)
|
|
6
6
|
Store.prototype.saveAsync = promisify(Store.prototype.save)
|
|
7
7
|
|
|
8
|
-
const sift = require('sift')
|
|
9
|
-
|
|
10
8
|
const util = require('../util')
|
|
11
9
|
|
|
12
10
|
module.exports = function (settings, collection) {
|
|
@@ -25,7 +23,7 @@ module.exports = function (settings, collection) {
|
|
|
25
23
|
const arr = Object.keys(data).map(function (id) {
|
|
26
24
|
return data[id]
|
|
27
25
|
})
|
|
28
|
-
let matches =
|
|
26
|
+
let matches = util.mongoSearch(arr, query)
|
|
29
27
|
if (orderby) {
|
|
30
28
|
matches = util.orderBy(matches, orderby)
|
|
31
29
|
}
|
|
@@ -96,7 +94,7 @@ module.exports = function (settings, collection) {
|
|
|
96
94
|
updateWithQuery: async function (query, update, options) {
|
|
97
95
|
const data = await store.allAsync()
|
|
98
96
|
const arr = Object.keys(data).map((id) => ({ _id: id, ...data[id]}) )
|
|
99
|
-
const matches =
|
|
97
|
+
const matches = util.mongoSearch(arr, query)
|
|
100
98
|
const promises = matches.map((doc) => {
|
|
101
99
|
util.mongoUpdate(doc, update)
|
|
102
100
|
return store.saveAsync(doc._id, doc)
|
package/db/memory.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
|
2
|
-
const sift = require('sift')
|
|
3
|
-
|
|
4
2
|
const util = require('../util')
|
|
5
3
|
|
|
6
4
|
module.exports = function (settings, collection) {
|
|
@@ -14,7 +12,7 @@ module.exports = function (settings, collection) {
|
|
|
14
12
|
},
|
|
15
13
|
find: async function (query, offset, limit, orderby, fields) {
|
|
16
14
|
const arr = Object.values(store)
|
|
17
|
-
let matches =
|
|
15
|
+
let matches = util.mongoSearch(arr, query)
|
|
18
16
|
if (orderby) {
|
|
19
17
|
matches = util.orderBy(matches, orderby)
|
|
20
18
|
}
|
|
@@ -61,7 +59,7 @@ module.exports = function (settings, collection) {
|
|
|
61
59
|
// https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/
|
|
62
60
|
updateWithQuery: async function (query, update, options) {
|
|
63
61
|
const arr = Object.keys(store).map((id) => ({ _id: id, ...store[id]}) )
|
|
64
|
-
const matches =
|
|
62
|
+
const matches = util.mongoSearch(arr, query)
|
|
65
63
|
matches.forEach((doc) => {
|
|
66
64
|
util.mongoUpdate(doc, update)
|
|
67
65
|
store[doc._id] = doc
|
package/db/postgres.js
CHANGED
|
@@ -1,100 +1,93 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const result = await pool.query(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (result.rowCount === 0) {
|
|
95
|
-
throw new util.ApiError(404, 'document not found')
|
|
96
|
-
}
|
|
97
|
-
return 'OK'
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
const util = require('../util')
|
|
2
|
+
|
|
3
|
+
module.exports = function (settings, collectionId, collection) {
|
|
4
|
+
const pool = util.getPgPool(settings.postgresql_uri)
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
init: async function () {
|
|
8
|
+
if (collection.plainStringIds) {
|
|
9
|
+
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id text primary key, data jsonb)')
|
|
10
|
+
} else {
|
|
11
|
+
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id uuid primary key, data jsonb)')
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
all: async function () {
|
|
15
|
+
return this.find({})
|
|
16
|
+
},
|
|
17
|
+
find: async function (rawQuery, offset, limit, orderby, fields) {
|
|
18
|
+
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
19
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
20
|
+
let query = 'SELECT ' + pgSelect + ' FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
21
|
+
if (typeof orderby !== 'undefined') {
|
|
22
|
+
query += ' ORDER BY '
|
|
23
|
+
query += util.mongoToPostgresOrderBy(collectionId, orderby)
|
|
24
|
+
}
|
|
25
|
+
if (typeof offset !== 'undefined') {
|
|
26
|
+
query += ' OFFSET ' + offset
|
|
27
|
+
}
|
|
28
|
+
if (typeof limit !== 'undefined') {
|
|
29
|
+
query += ' LIMIT ' + limit
|
|
30
|
+
}
|
|
31
|
+
const result = await pool.query(query)
|
|
32
|
+
return result.rows.map((row) => row.data)
|
|
33
|
+
},
|
|
34
|
+
count: async function(rawQuery, offset, limit) {
|
|
35
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
36
|
+
let query = 'SELECT COUNT(*) FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
37
|
+
if (typeof offset !== 'undefined') {
|
|
38
|
+
query += ' OFFSET ' + offset
|
|
39
|
+
}
|
|
40
|
+
if (typeof limit !== 'undefined') {
|
|
41
|
+
query += ' LIMIT ' + limit
|
|
42
|
+
}
|
|
43
|
+
const result = await pool.query(query)
|
|
44
|
+
return parseInt(result.rows[0].count)
|
|
45
|
+
},
|
|
46
|
+
get: async function (id, fields) {
|
|
47
|
+
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
48
|
+
const result = await pool.query(`SELECT ${pgSelect} FROM ${collectionId} WHERE id = $1`, [id])
|
|
49
|
+
if (result.rowCount === 0) {
|
|
50
|
+
throw new util.ApiError(404, 'document not found')
|
|
51
|
+
}
|
|
52
|
+
return result.rows[0].data
|
|
53
|
+
},
|
|
54
|
+
create: async function (data) {
|
|
55
|
+
util.addIdIfMissing(data)
|
|
56
|
+
try {
|
|
57
|
+
await pool.query('INSERT INTO ' + collectionId + ' (id, data) VALUES ($1, $2)', [data._id, data])
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e.message.includes('duplicate key')) {
|
|
60
|
+
throw new util.ApiError(409, 'document already exists')
|
|
61
|
+
}
|
|
62
|
+
throw e
|
|
63
|
+
}
|
|
64
|
+
return data._id
|
|
65
|
+
},
|
|
66
|
+
update: async function (id, data) {
|
|
67
|
+
if (typeof data._id === 'undefined') {
|
|
68
|
+
data._id = id
|
|
69
|
+
}
|
|
70
|
+
const result = await pool.query('UPDATE ' + collectionId + ' SET data=$2,id=$3 WHERE id=$1', [id, data, data._id])
|
|
71
|
+
if (result.rowCount === 0) {
|
|
72
|
+
throw new util.ApiError(404, 'document not found')
|
|
73
|
+
}
|
|
74
|
+
return data
|
|
75
|
+
},
|
|
76
|
+
// eslint-disable-next-line no-unused-vars
|
|
77
|
+
updateWithQuery: async function (query, update, options) {
|
|
78
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, query)
|
|
79
|
+
const updateSql = util.mongoToPostgresUpdate(collectionId, update)
|
|
80
|
+
const result = await pool.query('UPDATE ' + collectionId + ' SET data=' + updateSql + ' WHERE ' + pgWhere)
|
|
81
|
+
return {
|
|
82
|
+
matchedCount: result.rowCount
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
delete: async function (id) {
|
|
86
|
+
const result = await pool.query('DELETE FROM ' + collectionId + ' WHERE id=$1', [id])
|
|
87
|
+
if (result.rowCount === 0) {
|
|
88
|
+
throw new util.ApiError(404, 'document not found')
|
|
89
|
+
}
|
|
90
|
+
return 'OK'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/index.js
CHANGED
package/listeners_users.js
CHANGED
|
@@ -13,7 +13,7 @@ module.exports = async function(api) {
|
|
|
13
13
|
data.password = oldData.password // preserve password if not explicitly set
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
16
|
+
if (!auth.isHashed(data.password)) {
|
|
17
17
|
debug('hashing and replacing password in the user document.')
|
|
18
18
|
data.password = auth.createHash(data.password)
|
|
19
19
|
data.meta.password_last_updated_at = new Date().toISOString()
|
package/listeners_validation.js
CHANGED
|
@@ -18,13 +18,13 @@ module.exports = async function (api) {
|
|
|
18
18
|
const collections = await api.db.collection.all()
|
|
19
19
|
collections.forEach((collection) => {
|
|
20
20
|
const schema = addImplicitFields(collection.schema)
|
|
21
|
-
util.
|
|
21
|
+
util.addSchema(collection._id, schema)
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
// Load new and update validators as necessary
|
|
25
25
|
api.addCollectionListener('changed', 'collection', function updateSchemaValidators (req, collection, data) {
|
|
26
26
|
const schema = addImplicitFields(data.schema)
|
|
27
|
-
util.
|
|
27
|
+
util.addSchema(data._id, schema)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
api.addCollectionListener('get', ['collection', 'schemas'], function ensureIdAdded (req, collection, data) {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"friendly-errors-webpack-plugin": "1.7.0",
|
|
60
60
|
"html-webpack-plugin": "^4.0.0",
|
|
61
61
|
"mini-css-extract-plugin": "0.4.1",
|
|
62
|
-
"node-notifier": "
|
|
62
|
+
"node-notifier": "8.0.1",
|
|
63
63
|
"node-sass": "^6.0.1",
|
|
64
64
|
"optimize-css-assets-webpack-plugin": "5.0.0",
|
|
65
65
|
"ora": "3.0.0",
|
package/package.json
CHANGED
package/util.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const randomstring = require('randomstring')
|
|
2
1
|
const {v4} = require('uuid')
|
|
3
2
|
const debug = require('debug')('expressa')
|
|
4
3
|
const crypto = require('crypto')
|
|
@@ -6,6 +5,9 @@ const pg = require('pg')
|
|
|
6
5
|
const pgPools = {}
|
|
7
6
|
const dot = require('dot-object')
|
|
8
7
|
const mongoQuery = require('mongo-query')
|
|
8
|
+
const mongoToPostgres = require('mongo-query-to-postgres-jsonb')
|
|
9
|
+
const sift = require('sift')
|
|
10
|
+
const auth = require('./auth/index')
|
|
9
11
|
const Ajv = require('ajv')
|
|
10
12
|
const ajv = new Ajv({
|
|
11
13
|
allErrors: true,
|
|
@@ -31,9 +33,11 @@ ajv.addKeyword({
|
|
|
31
33
|
const formatKey = ajv.getKeyword('format')
|
|
32
34
|
formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
|
|
33
35
|
|
|
36
|
+
const schemas = {}
|
|
34
37
|
const schemaValidators = {}
|
|
35
38
|
|
|
36
|
-
exports.
|
|
39
|
+
exports.addSchema = function(collection, schema) {
|
|
40
|
+
schemas[collection] = schema
|
|
37
41
|
schemaValidators[collection] = ajv.compile(schema)
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -139,6 +143,27 @@ function _exclude(obj, source) {
|
|
|
139
143
|
return data
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
exports.mongoToPostgresSelect = function(collection, fields) {
|
|
147
|
+
const arrayFields = exports.getArrayPaths('', schemas[collection])
|
|
148
|
+
return fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.mongoToPostgresUpdate = function(collection, query) {
|
|
152
|
+
return mongoToPostgres.convertUpdate('data', query,false)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.mongoToPostgresWhere = function(collection, query) {
|
|
156
|
+
const arrayFields = exports.getArrayPaths('', schemas[collection])
|
|
157
|
+
return mongoToPostgres('data', query || {}, arrayFields)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
exports.mongoToPostgresOrderBy = function(collection, orderby) {
|
|
161
|
+
const normalizedOrderBy = exports.normalizeOrderBy(orderby)
|
|
162
|
+
return normalizedOrderBy.map((ordering) => {
|
|
163
|
+
return mongoToPostgres.convertDotNotation('data', ordering[0]) + (ordering[1] > 0 ? ' ASC' : ' DESC')
|
|
164
|
+
}).join(', ')
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
exports.mongoProject = function(record, projection) {
|
|
143
168
|
if (!projection || Object.keys(projection).length < 1) {
|
|
144
169
|
return record
|
|
@@ -152,6 +177,10 @@ exports.mongoProject = function(record, projection) {
|
|
|
152
177
|
return result
|
|
153
178
|
}
|
|
154
179
|
|
|
180
|
+
exports.mongoSearch = function(docs, query) {
|
|
181
|
+
return sift(query || {}, docs)
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
exports.mongoUpdate = function(doc, update) {
|
|
156
185
|
return mongoQuery(doc, {}, update)
|
|
157
186
|
}
|
|
@@ -166,6 +195,7 @@ exports.normalizeOrderBy = function(orderby) {
|
|
|
166
195
|
return [ordering[0], 1] // add 1 (default to ascending sort)
|
|
167
196
|
}
|
|
168
197
|
}
|
|
198
|
+
// if reaches here, is already normalized
|
|
169
199
|
return ordering
|
|
170
200
|
})
|
|
171
201
|
} else if (typeof orderby === 'object') {
|
|
@@ -423,3 +453,7 @@ exports.createPagePagination = function createPagePagination (pageData, page, pa
|
|
|
423
453
|
exports.getDatabaseOffset = function getDatabaseOffset(page, itemsPerPage) {
|
|
424
454
|
return (page - 1) * itemsPerPage
|
|
425
455
|
}
|
|
456
|
+
|
|
457
|
+
exports.createHash = auth.createHash
|
|
458
|
+
exports.isHashed = auth.isHashed
|
|
459
|
+
exports.doLogin = auth.doLogin
|