nitro-web 0.0.55 → 0.0.57
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/components/auth/auth.api.js +85 -91
- package/package.json +1 -1
- package/server/index.js +10 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import crypto from 'crypto'
|
|
2
|
+
import bcrypt from 'bcrypt'
|
|
3
3
|
import passport from 'passport'
|
|
4
4
|
import passportLocal from 'passport-local'
|
|
5
5
|
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
|
|
@@ -8,7 +8,7 @@ import jsonwebtoken from 'jsonwebtoken'
|
|
|
8
8
|
import { sendEmail } from 'nitro-web/server'
|
|
9
9
|
import { isArray, pick, isString, ucFirst, fullNameSplit } from 'nitro-web/util'
|
|
10
10
|
|
|
11
|
-
let
|
|
11
|
+
let authConfig = null
|
|
12
12
|
const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
|
|
13
13
|
|
|
14
14
|
export default {
|
|
@@ -34,10 +34,10 @@ export default {
|
|
|
34
34
|
function setup(middleware, _config) {
|
|
35
35
|
// Setup is called automatically when the server starts
|
|
36
36
|
// Set config values
|
|
37
|
-
const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword']
|
|
38
|
-
|
|
37
|
+
const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant']
|
|
38
|
+
authConfig = pick(_config, configKeys)
|
|
39
39
|
for (const key of ['clientUrl', 'emailFrom', 'env', 'name']) {
|
|
40
|
-
if (!
|
|
40
|
+
if (!authConfig[key]) throw new Error(`Missing config value for: config.${key}`)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
passport.use(
|
|
@@ -104,7 +104,7 @@ async function signup(req, res) {
|
|
|
104
104
|
try {
|
|
105
105
|
let user = await userCreate(req.body)
|
|
106
106
|
sendEmail({
|
|
107
|
-
config:
|
|
107
|
+
config: authConfig,
|
|
108
108
|
template: 'welcome',
|
|
109
109
|
to: `${ucFirst(user.firstName)}<${user.email}>`,
|
|
110
110
|
}).catch(console.error)
|
|
@@ -147,7 +147,7 @@ async function resetInstructions(req, res) {
|
|
|
147
147
|
|
|
148
148
|
res.json({})
|
|
149
149
|
sendEmail({
|
|
150
|
-
config:
|
|
150
|
+
config: authConfig,
|
|
151
151
|
template: 'reset-password',
|
|
152
152
|
to: `${ucFirst(user.firstName)}<${email}>`,
|
|
153
153
|
data: {
|
|
@@ -171,7 +171,7 @@ async function resetPassword(req, res) {
|
|
|
171
171
|
await db.user.update({
|
|
172
172
|
query: user._id,
|
|
173
173
|
data: {
|
|
174
|
-
password: await
|
|
174
|
+
password: await bcrypt.hash(password, 10),
|
|
175
175
|
resetToken: '',
|
|
176
176
|
},
|
|
177
177
|
blacklist: ['-resetToken', '-password'],
|
|
@@ -185,39 +185,75 @@ async function resetPassword(req, res) {
|
|
|
185
185
|
async function remove(req, res) {
|
|
186
186
|
try {
|
|
187
187
|
const uid = db.id(req.params.uid || 'badid')
|
|
188
|
-
|
|
189
|
-
// Get companies owned by user
|
|
190
|
-
const companyIdsOwned = (await db.company.find({
|
|
191
|
-
query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
|
|
192
|
-
project: { _id: 1 },
|
|
193
|
-
})).map(o => o._id)
|
|
194
|
-
|
|
195
188
|
// Check for active subscription first...
|
|
196
189
|
if (req.user.stripeSubscription?.status == 'active') {
|
|
197
190
|
throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
|
|
198
191
|
}
|
|
192
|
+
// // Get companies owned by user
|
|
193
|
+
// const companyIdsOwned = (await db.company.find({
|
|
194
|
+
// query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
|
|
195
|
+
// project: { _id: 1 },
|
|
196
|
+
// })).map(o => o._id)
|
|
199
197
|
|
|
200
|
-
if (companyIdsOwned.length) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await db.document.remove({ query: { company: { $in: companyIdsOwned }}})
|
|
205
|
-
await db.contact.remove({ query: { company: { $in: companyIdsOwned }}})
|
|
206
|
-
await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
|
|
207
|
-
await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
|
|
208
|
-
}
|
|
198
|
+
// if (companyIdsOwned.length) {
|
|
199
|
+
// await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
|
|
200
|
+
// await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
|
|
201
|
+
// }
|
|
209
202
|
await db.user.remove({ query: { _id: uid }})
|
|
210
203
|
// Logout now so that an error doesn't throw when naviating to /signout
|
|
211
204
|
req.logout()
|
|
212
|
-
res.send(`User: '${uid}'
|
|
205
|
+
res.send(`User: '${uid}' removed successfully`)
|
|
213
206
|
} catch (err) {
|
|
214
207
|
res.error(err)
|
|
215
208
|
}
|
|
216
209
|
}
|
|
217
210
|
|
|
218
|
-
/* ----
|
|
211
|
+
/* ---- Auth helpers ------------------------- */
|
|
219
212
|
|
|
220
|
-
async function
|
|
213
|
+
export async function findUserFromProvider(query, passwordToCheck) {
|
|
214
|
+
/**
|
|
215
|
+
* Find user for state (and verify password if signing in with email)
|
|
216
|
+
* @param {object} query - e.g. { email: 'test@test.com' }
|
|
217
|
+
* @param {string} <passwordToCheck> - password to test
|
|
218
|
+
*/
|
|
219
|
+
const isMultiTenant = !authConfig.isNotMultiTenant
|
|
220
|
+
const checkPassword = arguments.length > 1
|
|
221
|
+
const user = await db.user.findOne({
|
|
222
|
+
query: query,
|
|
223
|
+
blacklist: ['-password'],
|
|
224
|
+
populate: db.user.loginPopulate(),
|
|
225
|
+
_privateData: true,
|
|
226
|
+
})
|
|
227
|
+
if (isMultiTenant && user?.company) {
|
|
228
|
+
user.company = await db.company.findOne({
|
|
229
|
+
query: user.company,
|
|
230
|
+
populate: db.company.loginPopulate(),
|
|
231
|
+
_privateData: true,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
if (!user) {
|
|
235
|
+
throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
|
|
236
|
+
} else if (isMultiTenant && !user.company) {
|
|
237
|
+
throw new Error('The current company is no longer associated with this user')
|
|
238
|
+
} else if (isMultiTenant && user.company.status != 'active') {
|
|
239
|
+
throw new Error('This user is not associated with an active company')
|
|
240
|
+
} else {
|
|
241
|
+
if (checkPassword) {
|
|
242
|
+
if (!user.password) {
|
|
243
|
+
throw new Error('There is no password associated with this account, please try signing in with another method.')
|
|
244
|
+
}
|
|
245
|
+
const match = user.password ? await bcrypt.compare(passwordToCheck, user.password) : false
|
|
246
|
+
if (!match && !(authConfig.masterPassword && passwordToCheck == authConfig.masterPassword)) {
|
|
247
|
+
throw new Error('Email or password is incorrect.')
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Successful return user
|
|
251
|
+
delete user.password
|
|
252
|
+
return user
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function getStore(user) {
|
|
221
257
|
// Initial store
|
|
222
258
|
return {
|
|
223
259
|
user: user || undefined,
|
|
@@ -256,47 +292,29 @@ export function tokenParse(token) {
|
|
|
256
292
|
}
|
|
257
293
|
}
|
|
258
294
|
|
|
259
|
-
async function validatePassword(password='', password2) {
|
|
260
|
-
// let hasLowerChar = password.match(/[a-z]/)
|
|
261
|
-
// let hasUpperChar = password.match(/[A-Z]/)
|
|
262
|
-
// let hasNumber = password.match(/\d/)
|
|
263
|
-
// let hasSymbol = password.match(/\W/)
|
|
264
|
-
if (!password) {
|
|
265
|
-
throw [{ title: 'password', detail: 'This field is required.' }]
|
|
266
|
-
} else if (config.env !== 'development' && password.length < 8) {
|
|
267
|
-
throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
|
|
268
|
-
// } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
|
|
269
|
-
// throw {
|
|
270
|
-
// title: 'password',
|
|
271
|
-
// detail: 'You need to include uppercase and lowercase letters, and a number'
|
|
272
|
-
// }
|
|
273
|
-
} else if (typeof password2 != 'undefined' && password !== password2) {
|
|
274
|
-
throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
295
|
export async function userCreate({ name, business, email, password }) {
|
|
279
296
|
try {
|
|
280
297
|
const options = { blacklist: ['-_id'] }
|
|
298
|
+
const isMultiTenant = !authConfig.isNotMultiTenant
|
|
281
299
|
const userId = db.id()
|
|
282
|
-
const companyData = {
|
|
300
|
+
const companyData = isMultiTenant && {
|
|
283
301
|
_id: db.id(),
|
|
284
302
|
...(business ? { business } : {}),
|
|
285
303
|
users: [{ _id: userId, role: 'owner', status: 'active' }],
|
|
286
304
|
}
|
|
287
305
|
const userData = {
|
|
288
306
|
_id: userId,
|
|
289
|
-
company: companyData._id,
|
|
290
307
|
email: email,
|
|
291
308
|
firstName: fullNameSplit(name)[0],
|
|
292
309
|
lastName: fullNameSplit(name)[1],
|
|
293
|
-
password: password ? await
|
|
310
|
+
password: password ? await bcrypt.hash(password, 10) : undefined,
|
|
311
|
+
...(isMultiTenant ? { company: companyData._id } : {}),
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
// First validate the data so we don't have to create a transaction
|
|
297
315
|
const results = await Promise.allSettled([
|
|
298
316
|
db.user.validate(userData, options),
|
|
299
|
-
db.company.validate(companyData, options),
|
|
317
|
+
...(isMultiTenant ? [db.company.validate(companyData, options)] : []),
|
|
300
318
|
typeof password === 'undefined' ? Promise.resolve() : validatePassword(password),
|
|
301
319
|
])
|
|
302
320
|
|
|
@@ -310,7 +328,7 @@ export async function userCreate({ name, business, email, password }) {
|
|
|
310
328
|
|
|
311
329
|
// Insert company & user
|
|
312
330
|
await db.user.insert({ data: userData, ...options })
|
|
313
|
-
await db.company.insert({ data: companyData, ...options })
|
|
331
|
+
if (isMultiTenant) await db.company.insert({ data: companyData, ...options })
|
|
314
332
|
|
|
315
333
|
// Return the user
|
|
316
334
|
return await findUserFromProvider({ _id: userId })
|
|
@@ -324,45 +342,21 @@ export async function userCreate({ name, business, email, password }) {
|
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
344
|
|
|
327
|
-
export async function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
populate: db.company.loginPopulate(),
|
|
344
|
-
_privateData: true,
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
if (!user) {
|
|
348
|
-
throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
|
|
349
|
-
} else if (!user.company) {
|
|
350
|
-
throw new Error('The current company is no longer associated with this user')
|
|
351
|
-
} else if (user.company.status != 'active') {
|
|
352
|
-
throw new Error('This user is not associated with an active company')
|
|
353
|
-
} else {
|
|
354
|
-
if (checkPassword) {
|
|
355
|
-
if (!user.password) {
|
|
356
|
-
throw new Error('There is no password associated with this account, please try signing in with another method.')
|
|
357
|
-
}
|
|
358
|
-
const match = user.password ? await (await import('bcrypt')).compare(passwordToCheck, user.password) : false
|
|
359
|
-
if (!match && !(config.masterPassword && passwordToCheck == config.masterPassword)) {
|
|
360
|
-
throw new Error('Email or password is incorrect.')
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Successful return user
|
|
364
|
-
delete user.password
|
|
365
|
-
return user
|
|
345
|
+
export async function validatePassword(password='', password2) {
|
|
346
|
+
// let hasLowerChar = password.match(/[a-z]/)
|
|
347
|
+
// let hasUpperChar = password.match(/[A-Z]/)
|
|
348
|
+
// let hasNumber = password.match(/\d/)
|
|
349
|
+
// let hasSymbol = password.match(/\W/)
|
|
350
|
+
if (!password) {
|
|
351
|
+
throw [{ title: 'password', detail: 'This field is required.' }]
|
|
352
|
+
} else if (authConfig.env !== 'development' && password.length < 8) {
|
|
353
|
+
throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
|
|
354
|
+
// } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
|
|
355
|
+
// throw {
|
|
356
|
+
// title: 'password',
|
|
357
|
+
// detail: 'You need to include uppercase and lowercase letters, and a number'
|
|
358
|
+
// }
|
|
359
|
+
} else if (typeof password2 != 'undefined' && password !== password2) {
|
|
360
|
+
throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
|
|
366
361
|
}
|
|
367
362
|
}
|
|
368
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.57",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|
package/server/index.js
CHANGED
|
@@ -19,6 +19,15 @@ export { setupRouter } from './router.js'
|
|
|
19
19
|
export { sendEmail } from './email/index.js'
|
|
20
20
|
|
|
21
21
|
// Export api default controllers
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
default as auth,
|
|
24
|
+
findUserFromProvider,
|
|
25
|
+
signinAndGetState,
|
|
26
|
+
userCreate,
|
|
27
|
+
tokenCreate,
|
|
28
|
+
tokenParse,
|
|
29
|
+
validatePassword,
|
|
30
|
+
getStore,
|
|
31
|
+
} from '../components/auth/auth.api.js'
|
|
23
32
|
export { default as settings } from '../components/settings/settings.api.js'
|
|
24
33
|
export { default as stripe } from '../components/billing/stripe.api.js'
|