@bitfinex/bfx-facs-auth-google 1.2.3 → 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.
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+ const BaseRepository = require('./repository')
5
+ const PrivilegeRepository = require('./privilege-repo')
6
+
7
+ class AdminPrivilegeRepository extends BaseRepository {
8
+ static tableName = 'admin_privileges'
9
+
10
+ /**
11
+ *
12
+ * @param {import('sqlite3').Database} db
13
+ * @param {{useDb: boolean}} conf
14
+ */
15
+ constructor (db, conf) {
16
+ super(db, AdminPrivilegeRepository.tableName, conf)
17
+ }
18
+
19
+ /**
20
+ *
21
+ * @param {object} param0
22
+ * @param {number} param0.adminId
23
+ * @param {number} param0.privilegeId
24
+ * @returns {Promise<{admin_id, privilege_id}>}
25
+ */
26
+ add ({ adminId, privilegeId }) {
27
+ assert.ok(adminId && typeof adminId === 'number', 'Admin ID is a required Integer')
28
+ assert.ok(privilegeId && typeof privilegeId === 'number', 'Privilege ID is a required Integer')
29
+ return super.add({ admin_id: adminId, privilege_id: privilegeId })
30
+ }
31
+
32
+ /**
33
+ *
34
+ * @param {number} adminId
35
+ * @param {string} privilege
36
+ * @returns {Promise<object>}
37
+ */
38
+ findAdminPrivilege (adminId, privilege) {
39
+ return new Promise((resolve, reject) => {
40
+ this.db.get(
41
+ `SELECT ap.admin_id, ap.privilege_id, p.name as privilege_name
42
+ FROM ${this.tableName} ap
43
+ JOIN ${PrivilegeRepository.tableName} p ON p.id = ap.privilege_id
44
+ WHERE ap.admin_id=? AND p.name=?
45
+ LIMIT 1;`,
46
+ [adminId, privilege],
47
+ function (err, row) {
48
+ if (err) return reject(err)
49
+
50
+ return resolve(row)
51
+ }
52
+ )
53
+ })
54
+ }
55
+
56
+ getAdminPrivileges (adminId) {
57
+ return new Promise((resolve, reject) => {
58
+ const query = `
59
+ SELECT p.id, p.name
60
+ FROM ${this.tableName} ap
61
+ JOIN ${PrivilegeRepository.tableName} p ON p.id = ap.privilege_id
62
+ WHERE ap.admin_id = ?;
63
+ `
64
+ this.db.all(query, [adminId], function (err, rows) {
65
+ if (err) return reject(err)
66
+ return resolve(rows)
67
+ })
68
+ })
69
+ }
70
+
71
+ remove (adminId, privilegeId) {
72
+ return new Promise((resolve, reject) => {
73
+ const query = `
74
+ DELETE FROM ${this.tableName}
75
+ WHERE admin_id = ?
76
+ AND privilege_id = ?;
77
+ `
78
+ this.db.run(query, [adminId, privilegeId], function (err, rows) {
79
+ if (err) return reject(err)
80
+ return resolve(rows)
81
+ })
82
+ })
83
+ }
84
+ }
85
+
86
+ module.exports = AdminPrivilegeRepository
@@ -0,0 +1,428 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+ const crypto = require('crypto')
5
+ const { isNil } = require('@bitfinex/lib-js-util-base')
6
+ const BaseRepository = require('./repository')
7
+ const { UserError } = require('../errors')
8
+
9
+ /**
10
+ * @typedef {{
11
+ * email: string,
12
+ * level: number,
13
+ * readOnly?: boolean,
14
+ * blockPrivilege?: boolean,
15
+ * analyticsPrivilege?: boolean,
16
+ * manageAdminsPrivilege?: boolean,
17
+ * casesPrivilege?: boolean,
18
+ * fetchMotivationsPrivilege?: boolean,
19
+ * passwordResetToken?: string,
20
+ * passwordResetSentAt?: Date,
21
+ * company?: string,
22
+ * forms?: string[],
23
+ * whitelistedIps?: string[]
24
+ * }} BaseAdminT
25
+ * @typedef { BaseAdminT & { password: string }} AddAdminT
26
+ * @typedef { BaseAdminT & {
27
+ * active: boolean,
28
+ * id: number
29
+ * }} AddedAdminT
30
+ */
31
+
32
+ async function hash (password, salt = '') {
33
+ return new Promise((resolve, reject) => {
34
+ const computedSalt = salt || crypto.randomBytes(8).toString('hex')
35
+
36
+ crypto.scrypt(password, computedSalt, 64, (err, derivedKey) => {
37
+ if (err) reject(err)
38
+ resolve(computedSalt + ':' + derivedKey.toString('hex'))
39
+ })
40
+ })
41
+ }
42
+
43
+ async function verify (password, hash) {
44
+ return new Promise((resolve, reject) => {
45
+ const [salt, key] = hash.split(':')
46
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
47
+ if (err) reject(err)
48
+ resolve(key === derivedKey.toString('hex'))
49
+ })
50
+ })
51
+ }
52
+
53
+ function isValidDate (value) {
54
+ const date = new Date(value)
55
+ return !isNaN(date.getTime())
56
+ }
57
+
58
+ class AdminUserRepository extends BaseRepository {
59
+ static tableName = 'admin_users'
60
+ static FORMS_FIELD = 'forms'
61
+ static JSON_FIELDS = [AdminUserRepository.FORMS_FIELD, 'whitelistedIps']
62
+
63
+ static runSqlAtStart = [
64
+ `CREATE TABLE IF NOT EXISTS ${AdminUserRepository.tableName} (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ email TEXT UNIQUE NOT NULL,
67
+ password TEXT,
68
+ level INTEGER NOT NULL,
69
+ active TINYINTEGER DEFAULT 1,
70
+ readOnly TINYINTEGER,
71
+ blockPrivilege TINYINTEGER,
72
+ analyticsPrivilege TINYINTEGER,
73
+ manageAdminsPrivilege TINYINTEGER,
74
+ casesPrivilege TINYINTEGER,
75
+ passwordResetToken TEXT,
76
+ passwordResetSentAt DATETIME,
77
+ company TEXT,
78
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
79
+ ${AdminUserRepository.FORMS_FIELD} TEXT,
80
+ whitelistedIps TEXT
81
+ )`,
82
+ `CREATE UNIQUE INDEX IF NOT EXISTS uidx_email ON ${AdminUserRepository.tableName}(email ASC)`
83
+ ]
84
+
85
+ /**
86
+ *
87
+ * @param {import('sqlite3').Database} db
88
+ * @param {{useDb: boolean, hashSalt: string}} conf
89
+ */
90
+ constructor (db, conf) {
91
+ super(db, AdminUserRepository.tableName, conf)
92
+ }
93
+
94
+ /**
95
+ * @param { AddAdminT } user
96
+ * @returns { Promise<AddedAdminT> }
97
+ */
98
+ async add (user) {
99
+ const {
100
+ email,
101
+ password,
102
+ level,
103
+ readOnly,
104
+ blockPrivilege,
105
+ analyticsPrivilege,
106
+ manageAdminsPrivilege,
107
+ casesPrivilege,
108
+ fetchMotivationsPrivilege,
109
+ company,
110
+ whitelistedIps
111
+ } = user
112
+
113
+ assert.ok(typeof email === 'string', 'Email is required')
114
+ assert.ok(typeof level === 'number', 'Level must be a number')
115
+
116
+ if (password) {
117
+ assert.ok(typeof password === 'string', 'Password should be a string')
118
+ }
119
+
120
+ if (readOnly) {
121
+ assert.ok(typeof readOnly === 'boolean', 'readOnly should be a boolean')
122
+ }
123
+
124
+ if (blockPrivilege) {
125
+ assert.ok(typeof blockPrivilege === 'boolean', 'blockPrivilege should be a boolean')
126
+ }
127
+
128
+ if (analyticsPrivilege) {
129
+ assert.ok(typeof analyticsPrivilege === 'boolean', 'analyticsPrivilege should be a boolean')
130
+ }
131
+
132
+ if (manageAdminsPrivilege) {
133
+ assert.ok(typeof manageAdminsPrivilege === 'boolean', 'manageAdminsPrivilege should be a boolean')
134
+ }
135
+
136
+ if (casesPrivilege) {
137
+ assert.ok(typeof casesPrivilege === 'boolean', 'casesPrivilege should be a boolean')
138
+ }
139
+
140
+ if (fetchMotivationsPrivilege) {
141
+ assert.ok(typeof fetchMotivationsPrivilege === 'boolean', 'fetchMotivationsPrivilege should be a boolean')
142
+ }
143
+
144
+ if (company) {
145
+ assert.ok(typeof company === 'string', 'company should be a string')
146
+ }
147
+
148
+ if (whitelistedIps !== undefined) {
149
+ assert.ok(Array.isArray(whitelistedIps), 'whitelistedIps should be an array')
150
+ whitelistedIps.forEach((ip) => {
151
+ assert.ok(typeof ip === 'string', 'each whitelistedIps entry should be a string')
152
+ })
153
+ }
154
+
155
+ const adm = await this.getAdmin(email, false)
156
+ if (adm) throw new UserError('ADMIN_ACCOUNT_EXISTS')
157
+
158
+ const hashedPassword = password
159
+ ? await hash(password, this.conf.hashSalt)
160
+ : null
161
+
162
+ user.password = hashedPassword
163
+
164
+ const data = {
165
+ ...user,
166
+ ...Object.fromEntries(
167
+ Object.keys(user).map(key => [key, AdminUserRepository.JSON_FIELDS.includes(key) ? JSON.stringify(user[key]) : user[key]])
168
+ )
169
+ }
170
+ const saved = await super.add(data)
171
+
172
+ return {
173
+ email,
174
+ level,
175
+ readOnly,
176
+ blockPrivilege,
177
+ analyticsPrivilege,
178
+ manageAdminsPrivilege,
179
+ casesPrivilege,
180
+ fetchMotivationsPrivilege,
181
+ company,
182
+ whitelistedIps,
183
+ active: true,
184
+ id: saved.id
185
+ }
186
+ }
187
+
188
+ async update (email, user) {
189
+ const {
190
+ password,
191
+ level,
192
+ readOnly,
193
+ blockPrivilege,
194
+ analyticsPrivilege,
195
+ manageAdminsPrivilege,
196
+ casesPrivilege,
197
+ fetchMotivationsPrivilege,
198
+ company,
199
+ active,
200
+ whitelistedIps,
201
+ passwordResetToken,
202
+ passwordResetSentAt
203
+ } = user
204
+
205
+ assert.ok(typeof email === 'string', 'Email is required')
206
+
207
+ if (user.email) {
208
+ throw new UserError('Email cannot be updated')
209
+ }
210
+
211
+ if (password) {
212
+ throw new UserError('Use Change Password endpoint to update user password')
213
+ }
214
+
215
+ if (level) {
216
+ assert.ok(typeof level === 'number', 'Level must be a number')
217
+ }
218
+
219
+ if (readOnly) {
220
+ assert.ok(typeof readOnly === 'boolean', 'readOnly should be a boolean')
221
+ }
222
+
223
+ if (blockPrivilege) {
224
+ assert.ok(typeof blockPrivilege === 'boolean', 'blockPrivilege should be a boolean')
225
+ }
226
+
227
+ if (analyticsPrivilege) {
228
+ assert.ok(typeof analyticsPrivilege === 'boolean', 'analyticsPrivilege should be a boolean')
229
+ }
230
+
231
+ if (manageAdminsPrivilege) {
232
+ assert.ok(typeof manageAdminsPrivilege === 'boolean', 'manageAdminsPrivilege should be a boolean')
233
+ }
234
+
235
+ if (casesPrivilege) {
236
+ assert.ok(typeof casesPrivilege === 'boolean', 'casesPrivilege should be a boolean')
237
+ }
238
+
239
+ if (fetchMotivationsPrivilege) {
240
+ assert.ok(typeof fetchMotivationsPrivilege === 'boolean', 'fetchMotivationsPrivilege should be a boolean')
241
+ }
242
+
243
+ if (company) {
244
+ assert.ok(typeof company === 'string', 'company should be a string')
245
+ }
246
+
247
+ if (active) {
248
+ assert.ok(typeof active === 'boolean', 'active should be a boolean')
249
+ }
250
+
251
+ if (whitelistedIps !== undefined) {
252
+ assert.ok(Array.isArray(whitelistedIps), 'whitelistedIps should be an array')
253
+ whitelistedIps.forEach((ip) => {
254
+ assert.ok(typeof ip === 'string', 'each whitelistedIps entry should be a string')
255
+ })
256
+ }
257
+
258
+ if (!isNil(passwordResetToken)) {
259
+ assert.ok(typeof passwordResetToken === 'string', 'passwordResetToken should be a string')
260
+ }
261
+
262
+ if (!isNil(passwordResetSentAt)) {
263
+ assert.ok(isValidDate(passwordResetSentAt), 'passwordResetSentAt should be a valid date')
264
+ }
265
+
266
+ const adm = await this.getAdmin(email, !active)
267
+ if (!adm) throw new UserError('ADMIN_ACCOUNT_DOES_NOT_EXIST_OR_IS_NOT_ACTIVE')
268
+
269
+ return new Promise((resolve, reject) => {
270
+ const keys = Object.keys(user)
271
+
272
+ this.db.run(
273
+ `UPDATE ${this.tableName} SET ${keys.join(' = ?, ')} = ? WHERE id = ?`,
274
+ keys.map(key => AdminUserRepository.JSON_FIELDS.includes(key) ? JSON.stringify(user[key]) : user[key]).concat(adm.id),
275
+ function (err) {
276
+ if (err) return reject(err)
277
+
278
+ resolve(user)
279
+ }
280
+ )
281
+ })
282
+ }
283
+
284
+ async updatePassword (email, newPassword, oldPassword) {
285
+ assert.ok(typeof email === 'string', 'Email is required')
286
+ assert.ok(typeof newPassword === 'string', 'New Password is required')
287
+ assert.ok(typeof oldPassword === 'string', 'Old Password is required')
288
+
289
+ const adm = await this.getAdmin(email)
290
+ if (!adm) throw new UserError('ADMIN_ACCOUNT_DOES_NOT_EXIST_OR_IS_NOT_ACTIVE')
291
+
292
+ if (!(await verify(oldPassword, adm.password))) {
293
+ throw new UserError('INVALID_PASSWORD')
294
+ }
295
+
296
+ const password = await hash(newPassword, this.conf.hashSalt)
297
+
298
+ return new Promise((resolve, reject) => {
299
+ this.db.run(
300
+ `UPDATE ${this.tableName} SET password = ? WHERE id = ?`,
301
+ [password, adm.id],
302
+ function (err) {
303
+ if (err) return reject(err)
304
+
305
+ resolve(true)
306
+ }
307
+ )
308
+ })
309
+ }
310
+
311
+ async resetPassword (email, newPassword, passwordResetToken) {
312
+ assert.ok(typeof email === 'string', 'Email is required')
313
+ assert.ok(typeof newPassword === 'string', 'New Password is required')
314
+
315
+ const admin = await this.getAdmin(email)
316
+ if (!admin) throw new UserError('ADMIN_ACCOUNT_DOES_NOT_EXIST_OR_IS_NOT_ACTIVE')
317
+ if (admin.passwordResetToken !== passwordResetToken) throw new UserError('INVALID_passwordResetToken')
318
+
319
+ const expiryDate = new Date(admin.passwordResetSentAt)
320
+ expiryDate.setDate(expiryDate.getDate() + 1)
321
+ if (Date.now() > expiryDate) throw new UserError('RESET_LINK_EXPIRED')
322
+
323
+ const password = await hash(newPassword, this.conf.hashSalt)
324
+
325
+ return new Promise((resolve, reject) => {
326
+ this.db.run(
327
+ `UPDATE ${this.tableName} SET password = ? WHERE id = ?`,
328
+ [password, admin.id],
329
+ function (err) {
330
+ if (err) return reject(err)
331
+
332
+ resolve(true)
333
+ }
334
+ )
335
+ })
336
+ }
337
+
338
+ /**
339
+ * @param { string|number } emailOrId - Identifier used for searching the admin, it can be either the admin email address or its database id.
340
+ * @param { boolean } [active=true] - Flag for considering only the active users, it is `true` by default. If `false`, it will search through inactive users too.
341
+ * @param { boolean } [id=false] - Flag for searching as well by id criterion, it's `false` by default. If `true`, enables the mentioned behavior.
342
+ * @returns { Promise<BaseAdminT & { timestamp: Date, active: boolean }> }
343
+ */
344
+ getAdmin (emailOrId, active = true, id = false) {
345
+ return new Promise((resolve, reject) => {
346
+ const identifierCondition = id
347
+ ? '(LOWER(email) = ? OR id = ?)'
348
+ : 'LOWER(email) = ?'
349
+
350
+ const query = active
351
+ ? `SELECT * FROM ${this.tableName} WHERE ${identifierCondition} AND active = 1`
352
+ : `SELECT * FROM ${this.tableName} WHERE ${identifierCondition}`
353
+
354
+ const params = [String(emailOrId).toLowerCase()]
355
+ if (id) {
356
+ params.push(emailOrId)
357
+ }
358
+
359
+ this.db.get(query, params, (err, row) => {
360
+ if (err) return reject(err)
361
+ resolve(row)
362
+ })
363
+ })
364
+ }
365
+
366
+ /**
367
+ *
368
+ * @param {boolean?} active
369
+ * @param {string?} company
370
+ * @returns {Promise<string[]>}
371
+ */
372
+ async getAdminEmails (active, company) {
373
+ return new Promise((resolve, reject) => {
374
+ let whereClause = ''
375
+ const params = []
376
+ if (active) {
377
+ whereClause += 'active =?'
378
+ params.push(1)
379
+ }
380
+ if (company) {
381
+ if (whereClause.length) {
382
+ whereClause += ' AND '
383
+ }
384
+ whereClause += 'company =?'
385
+ params.push(company)
386
+ }
387
+
388
+ const query = active || company
389
+ ? `SELECT LOWER(email) AS email FROM ${this.tableName} WHERE ${whereClause} ORDER BY email ASC`
390
+ : `SELECT LOWER(email) AS email FROM ${this.tableName} ORDER BY email ASC`
391
+
392
+ this.db.all(query, params, (err, rows) => {
393
+ if (err) return reject(err)
394
+ resolve((rows || []).map(row => row.email))
395
+ })
396
+ })
397
+ }
398
+
399
+ /**
400
+ *
401
+ * @returns {Promise<Boolean>}
402
+ */
403
+ doesAdminDbHaveData () {
404
+ return new Promise((resolve, reject) => {
405
+ const query = `SELECT EXISTS(SELECT 1 FROM ${this.tableName}) as exist`
406
+ this.db.get(query, (err, row) => {
407
+ if (err) return reject(err)
408
+ resolve(row?.exist)
409
+ })
410
+ })
411
+ }
412
+
413
+ async remove (idOrEmail) {
414
+ return new Promise((resolve, reject) => {
415
+ this.db.serialize(() => {
416
+ const statement = this.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ? OR LOWER(email) = ?`)
417
+ statement.run([idOrEmail, `${idOrEmail}`.toLowerCase()])
418
+ statement.finalize(err => {
419
+ if (err) return reject(err)
420
+
421
+ resolve(idOrEmail)
422
+ })
423
+ })
424
+ })
425
+ }
426
+ }
427
+
428
+ module.exports = AdminUserRepository
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+ const BaseRepository = require('./repository')
5
+
6
+ class PrivilegeRepository extends BaseRepository {
7
+ static tableName = 'privileges'
8
+
9
+ /**
10
+ *
11
+ * @param {import('sqlite3').Database} db
12
+ * @param {{useDb: boolean}} conf
13
+ */
14
+ constructor (db, conf) {
15
+ super(db, PrivilegeRepository.tableName, conf)
16
+ }
17
+
18
+ /**
19
+ *
20
+ * @param {object} param0
21
+ * @param {string} param0.name
22
+ * @returns {Promise<{name: string}>}
23
+ */
24
+ add ({ name }) {
25
+ const trimmedName = name?.trim()
26
+ assert.ok(trimmedName && typeof trimmedName === 'string', 'Name is a required string')
27
+ return super.add({ name: trimmedName })
28
+ }
29
+ }
30
+
31
+ module.exports = PrivilegeRepository
@@ -0,0 +1,81 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+
5
+ class BaseRepository {
6
+ /**
7
+ *
8
+ * @param {import('sqlite3').Database} db
9
+ * @param {string} tableName
10
+ * @param {Record<string, any>} conf
11
+ */
12
+ constructor (db, tableName, conf) {
13
+ this.db = db
14
+ this.tableName = tableName
15
+ this.conf = conf
16
+
17
+ assert.ok(this.conf.useDB, 'Cannot use repository pattern if DB is not available')
18
+ }
19
+
20
+ /**
21
+ *
22
+ * @param {object} data
23
+ * @returns {Promise<data & {id: number}>}
24
+ */
25
+ add (data) {
26
+ return new Promise((resolve, reject) => {
27
+ const keys = Object.keys(data)
28
+
29
+ this.db.run(
30
+ `INSERT INTO ${this.tableName} (${keys.join(', ')}) VALUES (${Array(keys.length).fill('?').join(', ')})`,
31
+ Object.values(data),
32
+ function (err) {
33
+ if (err) return reject(err)
34
+
35
+ return resolve({ ...data, id: this.lastID })
36
+ }
37
+ )
38
+ })
39
+ }
40
+
41
+ /**
42
+ *
43
+ * @returns {Promise<Array>}
44
+ */
45
+ findAll () {
46
+ return new Promise((resolve, reject) => {
47
+ this.db.all(
48
+ `SELECT * FROM ${this.tableName}`,
49
+ [],
50
+ function (err, rows) {
51
+ if (err) return reject(err)
52
+
53
+ return resolve(rows)
54
+ }
55
+ )
56
+ })
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @param {number} id
62
+ * @returns
63
+ */
64
+ findById (id) {
65
+ assert.ok(this.conf.useDB, 'Cannot use repository pattern if DB is not available')
66
+
67
+ return new Promise((resolve, reject) => {
68
+ this.db.get(
69
+ `SELECT * FROM ${this.tableName} WHERE id=?`,
70
+ [id],
71
+ function (err, row) {
72
+ if (err) return reject(err)
73
+
74
+ return resolve(row)
75
+ }
76
+ )
77
+ })
78
+ }
79
+ }
80
+
81
+ module.exports = BaseRepository