@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.
- package/index.js +115 -329
- package/migrations.js +48 -0
- package/package.json +2 -2
- package/src/admin-privilege-repo.js +86 -0
- package/src/admin-user-repo.js +428 -0
- package/src/privilege-repo.js +31 -0
- package/src/repository.js +81 -0
- package/test/privileges.test.js +160 -0
- package/test/unit.js +291 -12
|
@@ -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
|