nitro-web 0.0.183 → 0.0.185

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/client/app.tsx CHANGED
@@ -319,8 +319,8 @@ async function beforeApp(config: Config) {
319
319
 
320
320
  export const middleware = {
321
321
  // Default middleware that can referenced from component routes
322
- isAdmin: (route: unknown, store: { user?: { type?: string, isAdmin?: boolean } }) => {
323
- if (store.user?.type?.match(/admin/) || store.user?.isAdmin) return
322
+ isAdmin: (route: unknown, store: { user?: { isAdmin?: boolean } }) => {
323
+ if (store.user?.isAdmin) return
324
324
  else if (store.user) return { redirect: '/signin?unauth' }
325
325
  else return { redirect: '/signin?signin' }
326
326
  },
@@ -328,7 +328,7 @@ export const middleware = {
328
328
  if (store.user?.company?.currentSubscription) return
329
329
  else return { redirect: '/plans/subscribe' }
330
330
  },
331
- isUser: (route: unknown, store: { user?: { type?: string } }) => {
331
+ isUser: (route: unknown, store: { user?: { isAdmin?: boolean } }) => {
332
332
  if (store.user) return
333
333
  else return { redirect: '/signin?signin' }
334
334
  },
package/client/index.ts CHANGED
@@ -16,6 +16,7 @@ export { createStore, exposedStoreData, preloadedStoreData, setStoreWrapper } fr
16
16
  export { Signin } from '../components/auth/signin'
17
17
  export { Signup } from '../components/auth/signup'
18
18
  export { ResetInstructions, ResetPassword } from '../components/auth/reset'
19
+ export { InviteConfirm } from '../components/auth/inviteConfirm'
19
20
  export { Dashboard } from '../components/dashboard/dashboard'
20
21
  export { NotFound } from '../components/partials/not-found'
21
22
  export { Styleguide } from '../components/partials/styleguide'
@@ -4,50 +4,46 @@ import bcrypt from 'bcrypt'
4
4
  import passport from 'passport'
5
5
  import passportLocal from 'passport-local'
6
6
  import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
7
- import db from 'monastery'
7
+ import db, { isId } from 'monastery'
8
8
  import jsonwebtoken from 'jsonwebtoken'
9
9
  import { sendEmail } from 'nitro-web/server'
10
10
  import { isArray, pick, ucFirst, fullNameSplit } from 'nitro-web/util'
11
+ // Todo: detect if the user is already invited to the company, instead of token error
11
12
 
12
- let authConfig = null
13
13
  const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
14
+ let authConfig = null
15
+ let auth = {
16
+ userFindFromProvider, userSigninGetStore, getStore,
17
+ userCreate, passwordValidate, tokenCreate, tokenParse, tokenSend,
18
+ tokenConfirmForReset, tokenConfirmForSingleTenant, tokenConfirmForMultiTenant,
19
+ }
14
20
 
15
21
  export const routes = {
16
22
  // Routes
17
23
  'get /api/store': [store],
18
24
  'get /api/signout': [signout],
19
- 'post /api/signin': [signin],
20
- 'post /api/signup': [signup],
25
+ 'post /api/signin': [signin], // [todo: route gaurd basic limiter]
26
+ 'post /api/signup': [signup], // [todo: route gaurd Altcha]
21
27
  'post /api/reset-instructions': [resetInstructions],
22
- 'post /api/reset-password': [resetConfirm],
28
+ 'post /api/reset-confirm': [resetConfirm], // was reset-password
23
29
  'post /api/invite-instructions': [inviteInstructions],
24
- 'post /api/invite-accept': [inviteConfirm],
30
+ 'post /api/invite-confirm': [inviteConfirm], // was invite-accept
25
31
  'delete /api/account/:uid': [remove],
26
-
27
- // todo:
28
- // We dont need all of these overridable, just signinAndGetStore, findUserFromProvider,
29
- // and getStore. So we will allow just these two to be passed around.
30
- // userCreate not needed, they can just create their own signup function.
31
- /// Maybe we can pass these into setup?
32
-
33
- // Overridable helpers
32
+ // Setup (called automatically when express starts)
34
33
  setup: setup,
35
- findUserFromProvider: findUserFromProvider,
36
- getStore: getStore,
37
- signinAndGetStore: signinAndGetStore,
38
- tokenCreate: tokenCreate,
39
- tokenParse: tokenParse,
40
- userCreate: userCreate,
41
- validatePassword: validatePassword,
42
- sendToken: sendToken,
43
- inviteOrResetConfirm: inviteOrResetConfirm,
44
34
  }
45
35
 
46
- function setup(middleware, _config) {
47
- // routes.setup is called automatically when express starts
48
- // Set config values
49
- const configKeys = ['baseUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant']
36
+ function setup(middleware, _config, helpers = {}) {
37
+ // Tip: you can pass in your own helpers to override the default helpers, `this` context contains all helpers.
38
+ // E.g. setup: (middleware, config, helpers) => authRoutes.setup(middleware, config, { getStore, ... })
39
+ const configKeys = ['baseUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant',
40
+ 'confirmInvites']
50
41
  authConfig = pick(_config, configKeys)
42
+ auth = { ...auth, ...helpers }
43
+ // Bind all the functions to the auth object (so the can be individually overridden and reference other helpers)
44
+ for (const key of Object.keys(auth)) {
45
+ if (typeof auth[key] === 'function') auth[key] = auth[key].bind(auth)
46
+ }
51
47
  for (const key of ['baseUrl', 'emailFrom', 'env', 'name']) {
52
48
  if (!authConfig[key]) throw new Error(`Missing config value for: config.${key}`)
53
49
  }
@@ -57,7 +53,7 @@ function setup(middleware, _config) {
57
53
  { usernameField: 'email' },
58
54
  async (email, password, next) => {
59
55
  try {
60
- const user = await this.findUserFromProvider({ email }, password)
56
+ const user = await auth.userFindFromProvider({ email }, password)
61
57
  next(null, user)
62
58
  } catch (err) {
63
59
  next(err.message)
@@ -74,7 +70,7 @@ function setup(middleware, _config) {
74
70
  },
75
71
  async (payload, done) => {
76
72
  try {
77
- const user = await this.findUserFromProvider({ _id: payload._id })
73
+ const user = await auth.userFindFromProvider({ _id: payload._id })
78
74
  if (!user) return done(null, false)
79
75
  return done(null, user)
80
76
  } catch (err) {
@@ -109,19 +105,14 @@ function setup(middleware, _config) {
109
105
  }
110
106
 
111
107
  async function store(req, res) {
112
- res.json(await this.getStore(req.user))
108
+ res.json(await auth.getStore(req.user))
113
109
  }
114
110
 
115
111
  async function signup(req, res) {
116
112
  try {
117
113
  const desktop = req.query.desktop
118
- let user = await this.userCreate(req.body)
119
- sendEmail({
120
- config: authConfig,
121
- template: 'welcome',
122
- to: `${ucFirst(user.firstName)}<${user.email}>`,
123
- }).catch(console.error)
124
- res.send(await this.signinAndGetStore(user, desktop, this.getStore))
114
+ const user = await auth.userCreate(req.body)
115
+ res.send(await auth.userSigninGetStore(user, desktop))
125
116
  } catch (err) {
126
117
  res.error(err)
127
118
  }
@@ -136,7 +127,7 @@ function signin(req, res) {
136
127
  if (err) return res.error(err)
137
128
  if (!user && info) return res.error('email', info.message)
138
129
  try {
139
- const response = await this.signinAndGetStore(user, desktop, this.getStore)
130
+ const response = await auth.userSigninGetStore(user, desktop)
140
131
  res.send(response)
141
132
  } catch (err) {
142
133
  res.error(err)
@@ -167,18 +158,102 @@ async function remove(req, res) {
167
158
  // }
168
159
  await db.user.remove({ query: { _id: uid }})
169
160
  // Logout now so that an error doesn't throw when naviating to /signout
170
- req.logout()
161
+ await new Promise((resolve, reject) => req.logout(err => err ? reject(err) : resolve()))
171
162
  res.send(`User: '${uid}' removed successfully`)
172
163
  } catch (err) {
173
164
  res.error(err)
174
165
  }
175
166
  }
176
167
 
168
+ export async function resetInstructions(req, res) {
169
+ try {
170
+ // const desktop = req.query.hasOwnProperty('desktop') ? '?desktop' : '' // see sendToken for future usage
171
+ let email = (req.body.email || '').trim().toLowerCase()
172
+ if (!email) throw { title: 'email', detail: 'The email you entered is incorrect.' }
173
+
174
+ let user = await db.user.findOne({ query: { email }, _privateData: true })
175
+ if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
176
+
177
+ // Send reset password email
178
+ await auth.tokenSend({ _id: user._id, email: user.email, firstName: user.firstName })
179
+ res.json({})
180
+ } catch (err) {
181
+ res.error(err)
182
+ }
183
+ }
184
+
185
+ export async function inviteInstructions(req, res) {
186
+ // Single-tenant:
187
+ //. - no user found: error (not supported, must be pre-created)
188
+ // - user pre-created: update user with token, and send them the token
189
+ // Multi-tenant:
190
+ // - user exists and confirmInvites=false: auto-add user to company.users
191
+ // - user exists and confirmInvites=true: add user to company.invites, and send them the token
192
+ //. - no user found: add user to company.invites and send them the token
193
+ try {
194
+ if (authConfig.isNotMultiTenant) {
195
+ const user = await db.user.findOne({ query: { _id: req.body._id }, _privateData: true })
196
+ if (!user) throw new Error('Please pre-create the user first, no user id found.')
197
+ await auth.tokenSend({ type: 'invite', _id: user._id, email: user.email, firstName: user.firstName })
198
+ res.json({})
199
+
200
+ } else {
201
+ const companyId = req.body.companyId
202
+ if (!req.user.isAdmin && (!companyId || req.user?.company?._id?.toString() !== companyId.toString())) {
203
+ throw new Error('You do not have permission to invite users to this company.')
204
+ }
205
+ const existingUser = await db.user.findOne({ query: { email: req.body.email } })
206
+ if (existingUser && !authConfig.confirmInvites) {
207
+ await db.company.update({
208
+ query: db.id(companyId),
209
+ $push: { users: { _id: existingUser._id, role: req.body.role, status: 'active' } },
210
+ })
211
+ } else {
212
+ await auth.tokenSend({
213
+ type: 'companyInvite', _id: companyId,
214
+ email: req.body.email,
215
+ firstName: existingUser?.firstName || req.body.firstName,
216
+ })
217
+ }
218
+ res.json({})
219
+ }
220
+ } catch (err) {
221
+ res.error(err)
222
+ }
223
+ }
224
+
225
+ export async function resetConfirm(req, res) {
226
+ try {
227
+ res.send(await auth.tokenConfirmForReset(req))
228
+ } catch (err) {
229
+ res.error(err)
230
+ }
231
+ }
232
+
233
+ export async function inviteConfirm(req, res) {
234
+ // single tenant:
235
+ // - user pre-created: update user with new password (and any other inviteConfirm.tsx form fields)
236
+ //. - no user found: error (not supported, must be pre-created)
237
+ // multi tenant:
238
+ // - user exists and confirmInvites=false: no-op (i.e no token, user already added)
239
+ // - user exists and confirmInvites=true: update company (invite.tsx should display 'Invite Accepted' and redirect to home)
240
+ // - no user found: create new user with new password (and any other inviteConfirm.tsx form fields)
241
+ try {
242
+ const result = authConfig.isNotMultiTenant
243
+ ? await auth.tokenConfirmForSingleTenant(req)
244
+ : await auth.tokenConfirmForMultiTenant(req)
245
+ res.send(result)
246
+ } catch (err) {
247
+ res.error(err)
248
+ }
249
+ }
250
+
177
251
  /* ---- Overridable helpers ------------------ */
178
252
 
179
- export async function findUserFromProvider(query, passwordToCheck) {
253
+ export async function userFindFromProvider(query, passwordToCheck) {
180
254
  /**
181
- * Find user for state (and verify password if signing in with email)
255
+ * Find user for state (and verify password if signing in with email).
256
+ * NOTE: the application needs to set user.company to the active company (if multi tenant)
182
257
  * @param {object} query - e.g. { email: 'test@test.com' }
183
258
  * @param {string} <passwordToCheck> - password to test
184
259
  */
@@ -199,10 +274,14 @@ export async function findUserFromProvider(query, passwordToCheck) {
199
274
  }
200
275
  if (!user) {
201
276
  throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
277
+ } else if (user.status && user.status !== 'active') {
278
+ throw new Error('This user account is not active.')
202
279
  } else if (isMultiTenant && !user.company) {
203
280
  throw new Error('The current company is no longer associated with this user')
204
281
  } else if (isMultiTenant && user.company.status != 'active') {
205
282
  throw new Error('This user is not associated with an active company')
283
+ } else if (isMultiTenant && !user.company.users?.find(u => u._id.toString() === user._id.toString())?.status === 'active') {
284
+ throw new Error('This user is not associated with an active company')
206
285
  } else {
207
286
  if (checkPassword) {
208
287
  if (!user.password) {
@@ -219,6 +298,15 @@ export async function findUserFromProvider(query, passwordToCheck) {
219
298
  }
220
299
  }
221
300
 
301
+ export async function userSigninGetStore(user, isDesktop) {
302
+ if (user.loginActive === false) throw { title: 'error', detail: 'This user is not available.' }
303
+ user.desktop = isDesktop
304
+
305
+ const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
306
+ const store = await this.getStore(user)
307
+ return { ...store, jwt }
308
+ }
309
+
222
310
  export async function getStore(user) {
223
311
  // Initial store
224
312
  return {
@@ -226,45 +314,43 @@ export async function getStore(user) {
226
314
  }
227
315
  }
228
316
 
229
- export async function signinAndGetStore(user, isDesktop, getStore) {
230
- if (user.loginActive === false) throw 'This user is not available.'
231
- if (!getStore) throw new Error('Please provide a getStore function')
232
- user.desktop = isDesktop
317
+ /* ---- Helpers (not easily overridable) ----- */
233
318
 
234
- const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
235
- const store = await getStore(user)
236
- return { ...store, jwt }
237
- }
238
-
239
- export async function userCreate({ business, password, ...userDataProp }) {
319
+ /**
320
+ * Creates a new user and company (if multi tenant and `user.company` is not an id)
321
+ * @param {object} userData - user data
322
+ * @param {string} [userData.password] - optional
323
+ * @param {string} [userData.password2] - optional, to confirm the password
324
+ * @param {string} [userData.company] - if multi tenant and `user.company` is not an id, create a new company
325
+ * @param {boolean} [skipSendEmail=false] - whether to skip sending the welcome email
326
+ * @returns {Promise<object>} - the created user
327
+ */
328
+ export async function userCreate({ password, password2, company, ...userDataProp }, skipSendEmail) {
240
329
  try {
241
- if (!this.findUserFromProvider) {
242
- throw new Error('this.findUserFromProvider doesn\'t exist, make sure the context is available when calling this function')
243
- }
244
-
245
- const options = { blacklist: ['-_id'] }
246
- const isMultiTenant = !authConfig.isNotMultiTenant
247
330
  const userId = db.id()
248
- const companyData = isMultiTenant && {
249
- _id: db.id(),
250
- ...(business ? { business } : {}),
331
+ const options = { blacklist: ['-_id'] }
332
+ const companyIsId = !authConfig.isNotMultiTenant && isId(company)
333
+
334
+ // Define new company data if applicable
335
+ const companyData = !authConfig.isNotMultiTenant && !companyIsId && {
336
+ _id: db.id(),
251
337
  users: [{ _id: userId, role: 'owner', status: 'active' }],
338
+ ...(company ? company : {}), // removed
252
339
  }
340
+
341
+ // Define user data
253
342
  const userData = {
254
343
  ...userDataProp,
255
344
  _id: userId,
256
- ...(userDataProp.name ? {
257
- firstName: fullNameSplit(userDataProp.name)[0],
258
- lastName: fullNameSplit(userDataProp.name)[1],
259
- } : {}),
260
345
  password: password ? await bcrypt.hash(password, 10) : undefined,
261
- ...(isMultiTenant ? { company: companyData._id } : {}),
346
+ ...(companyIsId ? { company: company } : (companyData ? { company: companyData._id } : {})), // AKA "active company"
347
+ ...(userDataProp.name ? { firstName: fullNameSplit(userDataProp.name)[0], lastName: fullNameSplit(userDataProp.name)[1] } : {}),
262
348
  }
263
349
  // First validate the data so we don't have to create a transaction
264
350
  const results = await Promise.allSettled([
265
351
  db.user.validate(userData, options),
266
- ...(isMultiTenant ? [db.company.validate(companyData, options)] : []),
267
- typeof password === 'undefined' ? Promise.resolve() : validatePassword(password),
352
+ typeof password === 'undefined' ? Promise.resolve() : this.passwordValidate(password, password2),
353
+ ...(companyData ? [db.company.validate(companyData, options)] : []),
268
354
  ])
269
355
 
270
356
  // Throw all the errors from at once
@@ -277,10 +363,19 @@ export async function userCreate({ business, password, ...userDataProp }) {
277
363
 
278
364
  // Insert company & user
279
365
  await db.user.insert({ data: userData, ...options })
280
- if (isMultiTenant) await db.company.insert({ data: companyData, ...options })
366
+ if (companyData) await db.company.insert({ data: companyData, ...options })
367
+
368
+ // Send welcome email
369
+ if (!skipSendEmail) {
370
+ sendEmail({
371
+ config: authConfig,
372
+ template: 'welcome',
373
+ to: `${ucFirst(userData.firstName)}<${userData.email}>`,
374
+ }).catch(console.error)
375
+ }
281
376
 
282
377
  // Return the user
283
- return await findUserFromProvider({ _id: userId })
378
+ return await this.userFindFromProvider({ _id: userId })
284
379
 
285
380
  } catch (err) {
286
381
  if (!isArray(err)) throw err
@@ -288,30 +383,7 @@ export async function userCreate({ business, password, ...userDataProp }) {
288
383
  }
289
384
  }
290
385
 
291
- export function tokenCreate(id) {
292
- return new Promise((resolve) => {
293
- crypto.randomBytes(16, (err, buff) => {
294
- let hash = buff.toString('hex') // 32 chars
295
- resolve(`${hash}${id || ''}:${Date.now()}`)
296
- })
297
- })
298
- }
299
-
300
- export function tokenParse(token) {
301
- let split = (token || '').split(':')
302
- let hash = split[0].slice(0, 32)
303
- let userId = split[0].slice(32)
304
- let time = split[1]
305
- if (!hash || !userId || !time) {
306
- throw { title: 'error', detail: 'Sorry your code is invalid.' }
307
- } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
308
- throw { title: 'error', detail: 'Sorry your code has timed out.' }
309
- } else {
310
- return userId
311
- }
312
- }
313
-
314
- export async function validatePassword(password='', password2) {
386
+ export async function passwordValidate(password='', password2) {
315
387
  // let hasLowerChar = password.match(/[a-z]/)
316
388
  // let hasUpperChar = password.match(/[A-Z]/)
317
389
  // let hasNumber = password.match(/\d/)
@@ -330,117 +402,125 @@ export async function validatePassword(password='', password2) {
330
402
  }
331
403
  }
332
404
 
333
-
334
-
335
-
336
- /* ---- Controllers -------------------------- */
337
-
338
- export async function resetInstructions(req, res) {
339
- try {
340
- // const desktop = req.query.hasOwnProperty('desktop') ? '?desktop' : '' // see sendToken for future usage
341
- let email = (req.body.email || '').trim().toLowerCase()
342
- if (!email) throw { title: 'email', detail: 'The email you entered is incorrect.' }
343
-
344
- let user = await db.user.findOne({ query: { email }, _privateData: true })
345
- if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
346
-
347
- // Send reset password email
348
- await sendToken({ type: 'reset', user: user })
349
- res.json({})
350
- } catch (err) {
351
- res.error(err)
352
- }
353
- }
354
-
355
- export async function inviteInstructions(req, res) {
356
- try {
357
- // const desktop = req.query.hasOwnProperty('desktop') ? '?desktop' : '' // see sendToken for future usage
358
- let user = await db.user.findOne({ query: { _id: req.params._id }, _privateData: true })
359
- if (!user) throw new Error('Invalid user id')
360
- // Send invite instructions email
361
- await sendToken({ type: 'invite', user: user })
362
- res.json({})
363
- } catch (err) {
364
- res.error(err)
365
- }
405
+ export function tokenCreate(modelName, id) {
406
+ return new Promise((resolve) => {
407
+ crypto.randomBytes(16, (err, buff) => {
408
+ let hash = buff.toString('hex') // 32 chars
409
+ resolve(`${hash}:${modelName}:${id || ''}:${Date.now()}`)
410
+ })
411
+ })
366
412
  }
367
413
 
368
- export async function inviteConfirm(req, res) {
369
- try {
370
- res.send(await this.inviteOrResetConfirm('invite', req))
371
- } catch (err) {
372
- res.error(err)
414
+ export function tokenParse(token, modelName, maxAgeMs = 1000 * 60 * 60 * 24) {
415
+ let [hash, modelNameParsed, id, time] = (token || '').split(':')
416
+ if (!hash || !id || !time) {
417
+ throw { title: 'error', detail: 'Sorry your code is invalid.' }
418
+ } else if (modelNameParsed !== modelName) {
419
+ throw { title: 'error', detail: 'Sorry we are detecting a token mismatch.' }
420
+ } else if (parseFloat(time) + maxAgeMs < Date.now()) {
421
+ throw { title: 'error', detail: 'Sorry your code has timed out.' }
422
+ } else {
423
+ return id
373
424
  }
374
425
  }
375
426
 
376
- export async function resetConfirm(req, res) {
377
- try {
378
- res.send(await this.inviteOrResetConfirm('reset', req))
379
- } catch (err) {
380
- res.error(err)
381
- }
427
+ export async function tokenConfirmForReset(req) {
428
+ return await this.tokenConfirmForSingleTenant(req, true)
382
429
  }
383
430
 
384
- /* ---- Helpers ------------------------------ */
385
-
386
- export async function inviteOrResetConfirm(type, req) {
387
- const { token, password, password2 } = req.body
388
- const name = type === 'invite' ? 'inviteToken' : 'resetToken'
431
+ export async function tokenConfirmForSingleTenant(req, isReset) {
432
+ const { token, password, password2, ...userData } = req.body
433
+ const tokenName = isReset ? 'resetToken' : 'inviteToken'
389
434
  const desktop = req.query.desktop
390
- const id = tokenParse(token)
391
- await validatePassword(password, password2)
435
+ const id = this.tokenParse(token, 'user')
436
+ await this.passwordValidate(password, password2)
392
437
 
393
- let user = await db.user.findOne({ query: id, blacklist: ['-' + name], _privateData: true })
394
- if (!user || user[name] !== token) throw new Error('Sorry your token is invalid or has already been used.')
438
+ const user = await db.user.findOne({ query: id, blacklist: ['-' + tokenName], _privateData: true })
439
+ if (!user || user[tokenName] !== token) throw new Error('Sorry your token is invalid or has already been used.')
395
440
 
396
441
  await db.user.update({
397
442
  query: user._id,
398
443
  data: {
399
444
  password: await bcrypt.hash(password, 10),
400
- [name]: '', // remove token
445
+ [tokenName]: '', // remove token
446
+ ...userData,
401
447
  },
402
- blacklist: ['-' + name, '-password'],
448
+ blacklist: ['-' + tokenName, '-password'],
403
449
  })
404
- const store = await this.signinAndGetStore({ ...user, [name]: undefined }, desktop, this.getStore)
405
- return store
450
+ return await this.userSigninGetStore({ ...user, [tokenName]: undefined }, desktop)
451
+ }
452
+
453
+ export async function tokenConfirmForMultiTenant(req) {
454
+ const { token, ...userData } = req.body
455
+ const desktop = req.query.desktop
456
+ const companyId = db.id(this.tokenParse(token, 'company'))
457
+
458
+ // Find the invite from the token (company.invites[] entry)
459
+ const company = await db.company.findOne({ query: { _id: companyId, 'invites.inviteToken': token }, _privateData: true })
460
+ if (!company) throw new Error('Sorry your token is invalid or has already been used.')
461
+ const invite = company.invites.find(inv => inv.inviteToken === token)
462
+
463
+ // Has the user already been added to the company (company.users[] entry)?
464
+ const existingUser = await db.user.findOne({ query: { email: userData.email }, _privateData: true })
465
+ if (existingUser && company.users.some(u => u._id.toString() === existingUser._id.toString())) {
466
+ throw new Error('This user has already been added to the company.')
467
+ }
468
+
469
+ // Find or create new user
470
+ const user = existingUser || await this.userCreate({ ...userData, company: companyId }, true) // AKA "active company"
471
+
472
+ // Add the user to the company
473
+ await db.company.update({
474
+ query: companyId,
475
+ $push: { users: { _id: user._id, role: invite.role, status: 'active' } }, // add user to company
476
+ $pull: { invites: { inviteToken: token } }, // remove invite
477
+ })
478
+
479
+ // Signin
480
+ return await this.userSigninGetStore(user, desktop)
406
481
  }
407
482
 
408
483
  /**
409
- * Checks if the user exists, updates the user with the invite token and sends the invite email
484
+ * Creates and sends a reset or invite token to a user or company
410
485
  * @param {object} options
411
- * @param {'reset' | 'invite'} options.type - The type of token to send (default: 'reset')
412
- * @param {{_id: string, email: string, firstName: string}} options.user - The user to send the invite email to
413
- * @param {function} [options.beforeUpdate] - callback hook to run before updating the user
414
- * @param {function} [options.beforeSendEmail] - callback hook to run before sending the email
486
+ * @param {'reset' | 'invite' | 'companyInvite'} options.type - token type (default: 'reset')
487
+ * @param {string} options._id - user or company id
488
+ * @param {string} options.email - recipient email
489
+ * @param {string} options.firstName - recipient first name
490
+ * @param {function} [options.beforeUpdate] - runs before updating the model with the token, return null to skip update
491
+ * @param {function} [options.beforeSendEmail] - runs before sending the email, receives (options, token)
415
492
  * @returns {Promise<{token: string, mailgunPromise: Promise<unknown>}>}
416
493
  */
417
- export async function sendToken({ type = 'reset', user, beforeUpdate, beforeSendEmail }) {
418
- if (!user?._id) throw new Error('user is required')
419
- if (!user?.email) throw new Error('user.email is required')
420
- if (!user?.firstName) throw new Error('user.firstName is required')
421
- const token = await tokenCreate(user._id)
422
-
423
- // get the data
424
- const data = beforeUpdate ? beforeUpdate({ [type + 'Token']: token }) : { [type + 'Token']: token }
425
- if (type === 'invite') data.isInvited = true
426
-
427
- // Update the user with the token
428
- const result = await db.user.update({
429
- query: { _id: user._id },
430
- data: data,
431
- blacklist: ['-' + type + 'Token'],
432
- })
433
-
434
- if (!result._output.matchedCount) {
435
- throw new Error('Invalid user id to send the token to')
494
+ export async function tokenSend({ type = 'reset', _id, email, firstName, beforeUpdate, beforeSendEmail }) {
495
+ if (!_id) throw new Error(`${type === 'companyInvite' ? 'company' : 'user'} id is required`)
496
+ if (!email) throw new Error('email is required')
497
+ if (!firstName) throw new Error('firstName is required')
498
+
499
+ const tokenName = type === 'companyInvite' ? 'inviteToken' : type + 'Token'
500
+ const modelName = type === 'companyInvite' ? 'company' : 'user'
501
+ const token = await this.tokenCreate(modelName, _id)
502
+ const _beforeUpdate = beforeUpdate || (o => o)
503
+
504
+ if (modelName === 'company') {
505
+ // For companies, add the token to company.invites[].inviteToken
506
+ await db.company.update({ query: db.id(_id), $pull: { invites: { email } } })
507
+ const result = await db.company.update({ query: db.id(_id), $push: { invites: _beforeUpdate({ email: email, inviteToken: token }) }})
508
+ if (!result._output.matchedCount) throw new Error('Invalid company id to update the token for')
509
+ } else {
510
+ // For users, add the token to user.inviteToken|resetToken
511
+ const result = await db[modelName].update({
512
+ query: db.id(_id),
513
+ $set: _beforeUpdate({ [tokenName]: token, isInvited: type === 'invite' ? true : undefined }),
514
+ })
515
+ if (!result._output.matchedCount) throw new Error('Invalid ' + modelName + ' id to update the token for')
436
516
  }
437
517
 
438
518
  // Send email
439
519
  const options = {
440
520
  config: authConfig,
441
- template: type === 'reset' ? 'reset-password' : 'invite-user',
442
- to: `${ucFirst(user.firstName)}<${user.email}>`,
443
- data: { token: token }, // + (req.query.hasOwnProperty('desktop') ? '?desktop' : '')
521
+ template: type === 'reset' ? 'reset-instructions' : 'invite-instructions',
522
+ to: `${ucFirst(firstName)}<${email}>`,
523
+ data: { token: token },
444
524
  }
445
525
  const mailgunPromise = sendEmail(beforeSendEmail ? beforeSendEmail(options, token) : options).catch(err => {
446
526
  console.error('sendEmail(..) mailgun error', err)