nitro-web 0.0.183 → 0.0.184
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 +3 -3
- package/client/index.ts +1 -0
- package/components/auth/auth.api.js +253 -177
- package/components/auth/inviteConfirm.tsx +106 -0
- package/components/auth/signup.tsx +6 -3
- package/package.json +1 -1
- package/server/models/company.js +10 -9
- package/server/models/user.js +3 -3
- package/server/router.js +2 -2
- package/types/components/auth/auth.api.d.ts +41 -39
- package/types/components/auth/auth.api.d.ts.map +1 -1
- package/types/server/models/company.d.ts +16 -2
- package/types/server/models/company.d.ts.map +1 -1
- package/types/server/models/user.d.ts +31 -34
- package/types/server/models/user.d.ts.map +1 -1
- package/types.ts +1 -1
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?: {
|
|
323
|
-
if (store.user?.
|
|
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?: {
|
|
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-
|
|
28
|
+
'post /api/reset-confirm': [resetConfirm], // was reset-password
|
|
23
29
|
'post /api/invite-instructions': [inviteInstructions],
|
|
24
|
-
'post /api/invite-
|
|
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
|
-
//
|
|
48
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
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
|
*/
|
|
@@ -219,6 +294,15 @@ export async function findUserFromProvider(query, passwordToCheck) {
|
|
|
219
294
|
}
|
|
220
295
|
}
|
|
221
296
|
|
|
297
|
+
export async function userSigninGetStore(user, isDesktop) {
|
|
298
|
+
if (user.loginActive === false) throw { title: 'error', detail: 'This user is not available.' }
|
|
299
|
+
user.desktop = isDesktop
|
|
300
|
+
|
|
301
|
+
const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
|
|
302
|
+
const store = await this.getStore(user)
|
|
303
|
+
return { ...store, jwt }
|
|
304
|
+
}
|
|
305
|
+
|
|
222
306
|
export async function getStore(user) {
|
|
223
307
|
// Initial store
|
|
224
308
|
return {
|
|
@@ -226,45 +310,43 @@ export async function getStore(user) {
|
|
|
226
310
|
}
|
|
227
311
|
}
|
|
228
312
|
|
|
229
|
-
|
|
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
|
|
313
|
+
/* ---- Helpers (not easily overridable) ----- */
|
|
233
314
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Creates a new user and company (if multi tenant and `user.company` is not an id)
|
|
317
|
+
* @param {object} userData - user data
|
|
318
|
+
* @param {string} [userData.password] - optional
|
|
319
|
+
* @param {string} [userData.password2] - optional, to confirm the password
|
|
320
|
+
* @param {string} [userData.company] - if multi tenant and `user.company` is not an id, create a new company
|
|
321
|
+
* @param {boolean} [skipSendEmail=false] - whether to skip sending the welcome email
|
|
322
|
+
* @returns {Promise<object>} - the created user
|
|
323
|
+
*/
|
|
324
|
+
export async function userCreate({ password, password2, company, ...userDataProp }, skipSendEmail) {
|
|
240
325
|
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
326
|
const userId = db.id()
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
327
|
+
const options = { blacklist: ['-_id'] }
|
|
328
|
+
const companyIsId = !authConfig.isNotMultiTenant && isId(company)
|
|
329
|
+
|
|
330
|
+
// Define new company data if applicable
|
|
331
|
+
const companyData = !authConfig.isNotMultiTenant && !companyIsId && {
|
|
332
|
+
_id: db.id(),
|
|
251
333
|
users: [{ _id: userId, role: 'owner', status: 'active' }],
|
|
334
|
+
...(company ? company : {}), // removed
|
|
252
335
|
}
|
|
336
|
+
|
|
337
|
+
// Define user data
|
|
253
338
|
const userData = {
|
|
254
339
|
...userDataProp,
|
|
255
340
|
_id: userId,
|
|
256
|
-
...(userDataProp.name ? {
|
|
257
|
-
firstName: fullNameSplit(userDataProp.name)[0],
|
|
258
|
-
lastName: fullNameSplit(userDataProp.name)[1],
|
|
259
|
-
} : {}),
|
|
260
341
|
password: password ? await bcrypt.hash(password, 10) : undefined,
|
|
261
|
-
...(
|
|
342
|
+
...(companyIsId ? { company: company } : (companyData ? { company: companyData._id } : {})), // AKA "active company"
|
|
343
|
+
...(userDataProp.name ? { firstName: fullNameSplit(userDataProp.name)[0], lastName: fullNameSplit(userDataProp.name)[1] } : {}),
|
|
262
344
|
}
|
|
263
345
|
// First validate the data so we don't have to create a transaction
|
|
264
346
|
const results = await Promise.allSettled([
|
|
265
347
|
db.user.validate(userData, options),
|
|
266
|
-
|
|
267
|
-
|
|
348
|
+
typeof password === 'undefined' ? Promise.resolve() : this.passwordValidate(password, password2),
|
|
349
|
+
...(companyData ? [db.company.validate(companyData, options)] : []),
|
|
268
350
|
])
|
|
269
351
|
|
|
270
352
|
// Throw all the errors from at once
|
|
@@ -277,10 +359,19 @@ export async function userCreate({ business, password, ...userDataProp }) {
|
|
|
277
359
|
|
|
278
360
|
// Insert company & user
|
|
279
361
|
await db.user.insert({ data: userData, ...options })
|
|
280
|
-
if (
|
|
362
|
+
if (companyData) await db.company.insert({ data: companyData, ...options })
|
|
363
|
+
|
|
364
|
+
// Send welcome email
|
|
365
|
+
if (!skipSendEmail) {
|
|
366
|
+
sendEmail({
|
|
367
|
+
config: authConfig,
|
|
368
|
+
template: 'welcome',
|
|
369
|
+
to: `${ucFirst(userData.firstName)}<${userData.email}>`,
|
|
370
|
+
}).catch(console.error)
|
|
371
|
+
}
|
|
281
372
|
|
|
282
373
|
// Return the user
|
|
283
|
-
return await
|
|
374
|
+
return await this.userFindFromProvider({ _id: userId })
|
|
284
375
|
|
|
285
376
|
} catch (err) {
|
|
286
377
|
if (!isArray(err)) throw err
|
|
@@ -288,30 +379,7 @@ export async function userCreate({ business, password, ...userDataProp }) {
|
|
|
288
379
|
}
|
|
289
380
|
}
|
|
290
381
|
|
|
291
|
-
export function
|
|
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) {
|
|
382
|
+
export async function passwordValidate(password='', password2) {
|
|
315
383
|
// let hasLowerChar = password.match(/[a-z]/)
|
|
316
384
|
// let hasUpperChar = password.match(/[A-Z]/)
|
|
317
385
|
// let hasNumber = password.match(/\d/)
|
|
@@ -330,117 +398,125 @@ export async function validatePassword(password='', password2) {
|
|
|
330
398
|
}
|
|
331
399
|
}
|
|
332
400
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
}
|
|
401
|
+
export function tokenCreate(modelName, id) {
|
|
402
|
+
return new Promise((resolve) => {
|
|
403
|
+
crypto.randomBytes(16, (err, buff) => {
|
|
404
|
+
let hash = buff.toString('hex') // 32 chars
|
|
405
|
+
resolve(`${hash}:${modelName}:${id || ''}:${Date.now()}`)
|
|
406
|
+
})
|
|
407
|
+
})
|
|
366
408
|
}
|
|
367
409
|
|
|
368
|
-
export
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
410
|
+
export function tokenParse(token, modelName, maxAgeMs = 1000 * 60 * 60 * 24) {
|
|
411
|
+
let [hash, modelNameParsed, id, time] = (token || '').split(':')
|
|
412
|
+
if (!hash || !id || !time) {
|
|
413
|
+
throw { title: 'error', detail: 'Sorry your code is invalid.' }
|
|
414
|
+
} else if (modelNameParsed !== modelName) {
|
|
415
|
+
throw { title: 'error', detail: 'Sorry we are detecting a token mismatch.' }
|
|
416
|
+
} else if (parseFloat(time) + maxAgeMs < Date.now()) {
|
|
417
|
+
throw { title: 'error', detail: 'Sorry your code has timed out.' }
|
|
418
|
+
} else {
|
|
419
|
+
return id
|
|
373
420
|
}
|
|
374
421
|
}
|
|
375
422
|
|
|
376
|
-
export async function
|
|
377
|
-
|
|
378
|
-
res.send(await this.inviteOrResetConfirm('reset', req))
|
|
379
|
-
} catch (err) {
|
|
380
|
-
res.error(err)
|
|
381
|
-
}
|
|
423
|
+
export async function tokenConfirmForReset(req) {
|
|
424
|
+
return await this.tokenConfirmForSingleTenant(req, true)
|
|
382
425
|
}
|
|
383
426
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const { token, password, password2 } = req.body
|
|
388
|
-
const name = type === 'invite' ? 'inviteToken' : 'resetToken'
|
|
427
|
+
export async function tokenConfirmForSingleTenant(req, isReset) {
|
|
428
|
+
const { token, password, password2, ...userData } = req.body
|
|
429
|
+
const tokenName = isReset ? 'resetToken' : 'inviteToken'
|
|
389
430
|
const desktop = req.query.desktop
|
|
390
|
-
const id = tokenParse(token)
|
|
391
|
-
await
|
|
431
|
+
const id = this.tokenParse(token, 'user')
|
|
432
|
+
await this.passwordValidate(password, password2)
|
|
392
433
|
|
|
393
|
-
|
|
394
|
-
if (!user || user[
|
|
434
|
+
const user = await db.user.findOne({ query: id, blacklist: ['-' + tokenName], _privateData: true })
|
|
435
|
+
if (!user || user[tokenName] !== token) throw new Error('Sorry your token is invalid or has already been used.')
|
|
395
436
|
|
|
396
437
|
await db.user.update({
|
|
397
438
|
query: user._id,
|
|
398
439
|
data: {
|
|
399
440
|
password: await bcrypt.hash(password, 10),
|
|
400
|
-
[
|
|
441
|
+
[tokenName]: '', // remove token
|
|
442
|
+
...userData,
|
|
401
443
|
},
|
|
402
|
-
blacklist: ['-' +
|
|
444
|
+
blacklist: ['-' + tokenName, '-password'],
|
|
403
445
|
})
|
|
404
|
-
|
|
405
|
-
|
|
446
|
+
return await this.userSigninGetStore({ ...user, [tokenName]: undefined }, desktop)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function tokenConfirmForMultiTenant(req) {
|
|
450
|
+
const { token, ...userData } = req.body
|
|
451
|
+
const desktop = req.query.desktop
|
|
452
|
+
const companyId = db.id(this.tokenParse(token, 'company'))
|
|
453
|
+
|
|
454
|
+
// Find the invite from the token (company.invites[] entry)
|
|
455
|
+
const company = await db.company.findOne({ query: { _id: companyId, 'invites.inviteToken': token }, _privateData: true })
|
|
456
|
+
if (!company) throw new Error('Sorry your token is invalid or has already been used.')
|
|
457
|
+
const invite = company.invites.find(inv => inv.inviteToken === token)
|
|
458
|
+
|
|
459
|
+
// Has the user already been added to the company (company.users[] entry)?
|
|
460
|
+
const existingUser = await db.user.findOne({ query: { email: userData.email }, _privateData: true })
|
|
461
|
+
if (existingUser && company.users.some(u => u._id.toString() === existingUser._id.toString())) {
|
|
462
|
+
throw new Error('This user has already been added to the company.')
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Find or create new user
|
|
466
|
+
const user = existingUser || await this.userCreate({ ...userData, company: companyId }, true) // AKA "active company"
|
|
467
|
+
|
|
468
|
+
// Add the user to the company
|
|
469
|
+
await db.company.update({
|
|
470
|
+
query: companyId,
|
|
471
|
+
$push: { users: { _id: user._id, role: invite.role, status: 'active' } }, // add user to company
|
|
472
|
+
$pull: { invites: { inviteToken: token } }, // remove invite
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Signin
|
|
476
|
+
return await this.userSigninGetStore(user, desktop)
|
|
406
477
|
}
|
|
407
478
|
|
|
408
479
|
/**
|
|
409
|
-
*
|
|
480
|
+
* Creates and sends a reset or invite token to a user or company
|
|
410
481
|
* @param {object} options
|
|
411
|
-
* @param {'reset' | 'invite'} options.type -
|
|
412
|
-
* @param {
|
|
413
|
-
* @param {
|
|
414
|
-
* @param {
|
|
482
|
+
* @param {'reset' | 'invite' | 'companyInvite'} options.type - token type (default: 'reset')
|
|
483
|
+
* @param {string} options._id - user or company id
|
|
484
|
+
* @param {string} options.email - recipient email
|
|
485
|
+
* @param {string} options.firstName - recipient first name
|
|
486
|
+
* @param {function} [options.beforeUpdate] - runs before updating the model with the token, return null to skip update
|
|
487
|
+
* @param {function} [options.beforeSendEmail] - runs before sending the email, receives (options, token)
|
|
415
488
|
* @returns {Promise<{token: string, mailgunPromise: Promise<unknown>}>}
|
|
416
489
|
*/
|
|
417
|
-
export async function
|
|
418
|
-
if (!
|
|
419
|
-
if (!
|
|
420
|
-
if (!
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
490
|
+
export async function tokenSend({ type = 'reset', _id, email, firstName, beforeUpdate, beforeSendEmail }) {
|
|
491
|
+
if (!_id) throw new Error(`${type === 'companyInvite' ? 'company' : 'user'} id is required`)
|
|
492
|
+
if (!email) throw new Error('email is required')
|
|
493
|
+
if (!firstName) throw new Error('firstName is required')
|
|
494
|
+
|
|
495
|
+
const tokenName = type === 'companyInvite' ? 'inviteToken' : type + 'Token'
|
|
496
|
+
const modelName = type === 'companyInvite' ? 'company' : 'user'
|
|
497
|
+
const token = await this.tokenCreate(modelName, _id)
|
|
498
|
+
const _beforeUpdate = beforeUpdate || (o => o)
|
|
499
|
+
|
|
500
|
+
if (modelName === 'company') {
|
|
501
|
+
// For companies, add the token to company.invites[].inviteToken
|
|
502
|
+
await db.company.update({ query: db.id(_id), $pull: { invites: { email } } })
|
|
503
|
+
const result = await db.company.update({ query: db.id(_id), $push: { invites: _beforeUpdate({ email: email, inviteToken: token }) }})
|
|
504
|
+
if (!result._output.matchedCount) throw new Error('Invalid company id to update the token for')
|
|
505
|
+
} else {
|
|
506
|
+
// For users, add the token to user.inviteToken|resetToken
|
|
507
|
+
const result = await db[modelName].update({
|
|
508
|
+
query: db.id(_id),
|
|
509
|
+
$set: _beforeUpdate({ [tokenName]: token, isInvited: type === 'invite' ? true : undefined }),
|
|
510
|
+
})
|
|
511
|
+
if (!result._output.matchedCount) throw new Error('Invalid ' + modelName + ' id to update the token for')
|
|
436
512
|
}
|
|
437
513
|
|
|
438
514
|
// Send email
|
|
439
515
|
const options = {
|
|
440
516
|
config: authConfig,
|
|
441
|
-
template: type === 'reset' ? 'reset-
|
|
442
|
-
to: `${ucFirst(
|
|
443
|
-
data: { token: token },
|
|
517
|
+
template: type === 'reset' ? 'reset-instructions' : 'invite-instructions',
|
|
518
|
+
to: `${ucFirst(firstName)}<${email}>`,
|
|
519
|
+
data: { token: token },
|
|
444
520
|
}
|
|
445
521
|
const mailgunPromise = sendEmail(beforeSendEmail ? beforeSendEmail(options, token) : options).catch(err => {
|
|
446
522
|
console.error('sendEmail(..) mailgun error', err)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Topbar, Field, FormError, Button, request, onChange, getResponseErrors, showError } from 'nitro-web'
|
|
2
|
+
import { Errors } from 'nitro-web/types'
|
|
3
|
+
import { Fragment, useEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
type InviteConfirmProps = {
|
|
6
|
+
className?: string,
|
|
7
|
+
elements?: { Button?: typeof Button },
|
|
8
|
+
redirectTo?: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function InviteConfirm({ className, elements, redirectTo }: InviteConfirmProps) {
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const params = useParams()
|
|
14
|
+
const [store, setStore] = useTracked()
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
16
|
+
const [accepted, setAccepted] = useState(false)
|
|
17
|
+
const [state, setState] = useState(() => ({
|
|
18
|
+
firstName: '',
|
|
19
|
+
lastName: '',
|
|
20
|
+
password: '',
|
|
21
|
+
password2: '',
|
|
22
|
+
token: params.token,
|
|
23
|
+
errors: [] as Errors,
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
const Elements = {
|
|
27
|
+
Button: elements?.Button || Button,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-confirm on mount for already signed-in users
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (store.user) submit({ token: params.token })
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
async function submit(data: object, event?: React.FormEvent<HTMLFormElement>) {
|
|
36
|
+
try {
|
|
37
|
+
if (isLoading) return
|
|
38
|
+
const result = await request('post /api/invite-confirm', data, event, setIsLoading, setState)
|
|
39
|
+
setStore((s) => ({ ...s, ...result }))
|
|
40
|
+
setAccepted(true)
|
|
41
|
+
setTimeout(() => navigate(redirectTo || '/'), 5000)
|
|
42
|
+
} catch (e) {
|
|
43
|
+
showError(setStore, e)
|
|
44
|
+
setState((s) => ({ ...s, errors: getResponseErrors(e) }))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (store.user) {
|
|
49
|
+
return (
|
|
50
|
+
<div className={className}>
|
|
51
|
+
<div class="py-12 text-center">
|
|
52
|
+
{accepted ? (
|
|
53
|
+
<Fragment>
|
|
54
|
+
<p class="text-lg font-semibold">Your invite has been accepted.</p>
|
|
55
|
+
<p class="text-sm text-gray-500 mt-1">You'll be redirected back to the <Link to="/">home page</Link> shortly...</p>
|
|
56
|
+
</Fragment>
|
|
57
|
+
) : isLoading ? (
|
|
58
|
+
<Fragment>
|
|
59
|
+
<p class="text-lg font-semibold">Accepting your invite...</p>
|
|
60
|
+
<p class="text-sm text-gray-500 mt-1">Please wait while we confirm your invite.</p>
|
|
61
|
+
</Fragment>
|
|
62
|
+
) : (
|
|
63
|
+
<Fragment>
|
|
64
|
+
<p class="text-lg font-semibold mb-2">Oops! Something went wrong.</p>
|
|
65
|
+
<span class="text-sm text-red-500 bg-red-50 p-1 rounded-md mt-1">{state.errors.map((error) => error.detail).join(', ')}</span>
|
|
66
|
+
</Fragment>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={className}>
|
|
75
|
+
<Topbar title={<Fragment>Accept Your Invite</Fragment>} />
|
|
76
|
+
|
|
77
|
+
<form onSubmit={(e) => submit(state, e)} class="mb-0">
|
|
78
|
+
<div class="grid grid-cols-2 gap-6">
|
|
79
|
+
<div>
|
|
80
|
+
<label for="firstName">First Name</label>
|
|
81
|
+
<Field name="firstName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your first name..." />
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<label for="lastName">Last Name</label>
|
|
85
|
+
<Field name="lastName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your last name..." />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<label for="password">Choose a Password</label>
|
|
90
|
+
<Field name="password" type="password" state={state} onChange={(e) => onChange(e, setState)} />
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<label for="password2">Repeat Your Password</label>
|
|
94
|
+
<Field name="password2" type="password" state={state} onChange={(e) => onChange(e, setState)} />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="mb-14">
|
|
98
|
+
Already have an account? <Link to="/signin" class="underline2 is-active">Sign in here</Link> first then revisit this link.
|
|
99
|
+
<FormError state={state} className="pt-2" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<Elements.Button class="w-full" isLoading={isLoading} type="submit">Accept Invite & Create Account</Elements.Button>
|
|
103
|
+
</form>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -15,7 +15,7 @@ export function Signup({ className, elements, redirectTo }: signupProps) {
|
|
|
15
15
|
const [state, setState] = useState({
|
|
16
16
|
email: injectedConfig.env === 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
17
17
|
name: injectedConfig.env === 'development' ? 'Bruce Wayne' : '',
|
|
18
|
-
business: { name: injectedConfig.env === 'development' ? 'Wayne Enterprises' : '' },
|
|
18
|
+
company: { business: { name: injectedConfig.env === 'development' ? 'Wayne Enterprises' : '' } },
|
|
19
19
|
password: injectedConfig.env === 'development' ? '' : '',
|
|
20
20
|
errors: [] as Errors,
|
|
21
21
|
})
|
|
@@ -49,8 +49,11 @@ export function Signup({ className, elements, redirectTo }: signupProps) {
|
|
|
49
49
|
/>
|
|
50
50
|
</div>
|
|
51
51
|
<div>
|
|
52
|
-
<label for="business.name">Company Name</label>
|
|
53
|
-
<Field name="business.name" placeholder="E.g. Wayne Enterprises" state={state}
|
|
52
|
+
<label for="company.business.name">Company Name</label>
|
|
53
|
+
<Field name="company.business.name" placeholder="E.g. Wayne Enterprises" state={state}
|
|
54
|
+
onChange={(e) => onChange(e, setState)}
|
|
55
|
+
errorTitle={/business\.name/}
|
|
56
|
+
/>
|
|
54
57
|
</div>
|
|
55
58
|
</div>
|
|
56
59
|
<div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.184",
|
|
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/models/company.js
CHANGED
|
@@ -13,17 +13,20 @@ export default {
|
|
|
13
13
|
phone: { type: 'string' },
|
|
14
14
|
website: { type: 'string', isURL: true },
|
|
15
15
|
},
|
|
16
|
-
status: { type: 'string', default: 'active', enum: ['active', 'unpaid', 'deleted'] },
|
|
16
|
+
status: { type: 'string', default: 'active', enum: ['active', 'unpaid', 'deleted'], required: true },
|
|
17
17
|
users: [{
|
|
18
|
-
_id: { model: 'user' },
|
|
19
|
-
role: { type: 'string', enum: ['owner', 'manager', 'accountant'] },
|
|
20
|
-
status: { type: 'string',
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
_id: { model: 'user', required: true },
|
|
19
|
+
role: { type: 'string', enum: ['owner', 'manager', 'accountant'], required: true },
|
|
20
|
+
status: { type: 'string', default: 'active', enum: ['active', 'deleted'], required: true },
|
|
21
|
+
}],
|
|
22
|
+
invites: [{
|
|
23
|
+
email: { type: 'email', required: true },
|
|
24
|
+
role: { type: 'string', enum: ['owner', 'manager', 'accountant'], required: true },
|
|
25
|
+
inviteToken: { type: 'string', required: true },
|
|
23
26
|
}],
|
|
24
27
|
},
|
|
25
28
|
|
|
26
|
-
findBL: ['
|
|
29
|
+
findBL: ['invites.token'],
|
|
27
30
|
updateBL: ['status', 'users'],
|
|
28
31
|
|
|
29
32
|
afterFind: [
|
|
@@ -34,8 +37,6 @@ export default {
|
|
|
34
37
|
for (let i=data.users.length; i--;) {
|
|
35
38
|
const user = data.users[i]
|
|
36
39
|
const userExpanded = data.usersExpanded.find(o => String(o._id) == String(user._id))
|
|
37
|
-
// console.log(userExpanded)
|
|
38
|
-
if (user.inviteEmail) user.email = user.inviteEmail
|
|
39
40
|
if (userExpanded) Object.assign(user, { ...userExpanded, name: fullName(userExpanded) })
|
|
40
41
|
}
|
|
41
42
|
delete data.usersExpanded
|
package/server/models/user.js
CHANGED
|
@@ -5,8 +5,9 @@ export default {
|
|
|
5
5
|
|
|
6
6
|
fields: {
|
|
7
7
|
avatar: { type: 'image' },
|
|
8
|
-
company: { model: 'company', required: true },
|
|
8
|
+
company: { model: 'company', required: true }, // AKA "active company"
|
|
9
9
|
email: { type: 'email', required: true, index: 'unique' },
|
|
10
|
+
isAdmin: { type: 'boolean', default: false },
|
|
10
11
|
isInvited: { type: 'boolean' },
|
|
11
12
|
firstName: { type: 'string', required: true },
|
|
12
13
|
lastName: { type: 'string', required: true },
|
|
@@ -14,7 +15,6 @@ export default {
|
|
|
14
15
|
stripeCustomer: { type: 'any' },
|
|
15
16
|
stripeSubscription: { type: 'any' },
|
|
16
17
|
stripeIntents: { type: 'any' },
|
|
17
|
-
type: { type: 'string', default: 'user', enum: ['user', 'admin'] },
|
|
18
18
|
usedFreeTrial: { type: 'boolean', default: false },
|
|
19
19
|
// hidden fields
|
|
20
20
|
password: { type: 'string', minLength: 6 },
|
|
@@ -23,7 +23,7 @@ export default {
|
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
findBL: ['password', 'inviteToken', 'resetToken'],
|
|
26
|
-
updateBL: ['password', 'inviteToken', 'resetToken', 'company', 'status', 'stripeSubscription', '
|
|
26
|
+
updateBL: ['password', 'inviteToken', 'resetToken', 'company', 'status', 'stripeSubscription', 'isAdmin', 'usedFreeTrial'],
|
|
27
27
|
|
|
28
28
|
messages: {
|
|
29
29
|
lastName: {
|
package/server/router.js
CHANGED
|
@@ -404,7 +404,7 @@ export const middleware = {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
export function isAdminUser(req) {
|
|
407
|
-
return
|
|
407
|
+
return req.user?.isAdmin ? true : false
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
export function isValidUserOrRespond(req, res) {
|
|
@@ -424,7 +424,7 @@ export function isValidUserOrRespond(req, res) {
|
|
|
424
424
|
function isValidParamCompanyUserOrRespond(req, res, checkIsOwner = false) {
|
|
425
425
|
const _company = req.user?.company?._id?.toString() == req.params.cid ? req.user.company : false
|
|
426
426
|
const company = _company || req.user?.companies?.find((o) => o._id.toString() == req.params.cid)
|
|
427
|
-
const isCompanyOwner = company?.users?.find((o) => o._id.toString() == req.user?._id?.toString() && o.
|
|
427
|
+
const isCompanyOwner = company?.users?.find((o) => o._id.toString() == req.user?._id?.toString() && o.role === 'owner')
|
|
428
428
|
if (!isValidUserOrRespond(req, res)) return
|
|
429
429
|
else if (!isAdminUser(req) && !company) res.unauthorized('You are not authorised to make this request.')
|
|
430
430
|
else if (!isAdminUser(req) && checkIsOwner && !isCompanyOwner) res.unauthorized('Only owners can make this request.')
|
|
@@ -1,37 +1,48 @@
|
|
|
1
|
-
export function findUserFromProvider(query: any, passwordToCheck: any, ...args: any[]): Promise<any>;
|
|
2
|
-
export function getStore(user: any): Promise<{
|
|
3
|
-
user: any;
|
|
4
|
-
}>;
|
|
5
|
-
export function signinAndGetStore(user: any, isDesktop: any, getStore: any): Promise<any>;
|
|
6
|
-
export function userCreate({ business, password, ...userDataProp }: {
|
|
7
|
-
[x: string]: any;
|
|
8
|
-
business: any;
|
|
9
|
-
password: any;
|
|
10
|
-
}): Promise<any>;
|
|
11
|
-
export function tokenCreate(id: any): Promise<any>;
|
|
12
|
-
export function tokenParse(token: any): any;
|
|
13
|
-
export function validatePassword(password: string, password2: any): Promise<void>;
|
|
14
1
|
export function resetInstructions(req: any, res: any): Promise<void>;
|
|
15
2
|
export function inviteInstructions(req: any, res: any): Promise<void>;
|
|
16
|
-
export function inviteConfirm(req: any, res: any): Promise<void>;
|
|
17
3
|
export function resetConfirm(req: any, res: any): Promise<void>;
|
|
18
|
-
export function
|
|
4
|
+
export function inviteConfirm(req: any, res: any): Promise<void>;
|
|
5
|
+
export function userFindFromProvider(query: any, passwordToCheck: any, ...args: any[]): Promise<any>;
|
|
6
|
+
export function userSigninGetStore(user: any, isDesktop: any): Promise<any>;
|
|
7
|
+
export function getStore(user: any): Promise<{
|
|
8
|
+
user: any;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new user and company (if multi tenant and `user.company` is not an id)
|
|
12
|
+
* @param {object} userData - user data
|
|
13
|
+
* @param {string} [userData.password] - optional
|
|
14
|
+
* @param {string} [userData.password2] - optional, to confirm the password
|
|
15
|
+
* @param {string} [userData.company] - if multi tenant and `user.company` is not an id, create a new company
|
|
16
|
+
* @param {boolean} [skipSendEmail=false] - whether to skip sending the welcome email
|
|
17
|
+
* @returns {Promise<object>} - the created user
|
|
18
|
+
*/
|
|
19
|
+
export function userCreate({ password, password2, company, ...userDataProp }: {
|
|
20
|
+
password?: string;
|
|
21
|
+
password2?: string;
|
|
22
|
+
company?: string;
|
|
23
|
+
}, skipSendEmail?: boolean): Promise<object>;
|
|
24
|
+
export function passwordValidate(password: string, password2: any): Promise<void>;
|
|
25
|
+
export function tokenCreate(modelName: any, id: any): Promise<any>;
|
|
26
|
+
export function tokenParse(token: any, modelName: any, maxAgeMs?: number): any;
|
|
27
|
+
export function tokenConfirmForReset(req: any): Promise<any>;
|
|
28
|
+
export function tokenConfirmForSingleTenant(req: any, isReset: any): Promise<any>;
|
|
29
|
+
export function tokenConfirmForMultiTenant(req: any): Promise<any>;
|
|
19
30
|
/**
|
|
20
|
-
*
|
|
31
|
+
* Creates and sends a reset or invite token to a user or company
|
|
21
32
|
* @param {object} options
|
|
22
|
-
* @param {'reset' | 'invite'} options.type -
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {
|
|
33
|
+
* @param {'reset' | 'invite' | 'companyInvite'} options.type - token type (default: 'reset')
|
|
34
|
+
* @param {string} options._id - user or company id
|
|
35
|
+
* @param {string} options.email - recipient email
|
|
36
|
+
* @param {string} options.firstName - recipient first name
|
|
37
|
+
* @param {function} [options.beforeUpdate] - runs before updating the model with the token, return null to skip update
|
|
38
|
+
* @param {function} [options.beforeSendEmail] - runs before sending the email, receives (options, token)
|
|
26
39
|
* @returns {Promise<{token: string, mailgunPromise: Promise<unknown>}>}
|
|
27
40
|
*/
|
|
28
|
-
export function
|
|
29
|
-
type: "reset" | "invite";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
firstName: string;
|
|
34
|
-
};
|
|
41
|
+
export function tokenSend({ type, _id, email, firstName, beforeUpdate, beforeSendEmail }: {
|
|
42
|
+
type: "reset" | "invite" | "companyInvite";
|
|
43
|
+
_id: string;
|
|
44
|
+
email: string;
|
|
45
|
+
firstName: string;
|
|
35
46
|
beforeUpdate?: Function;
|
|
36
47
|
beforeSendEmail?: Function;
|
|
37
48
|
}): Promise<{
|
|
@@ -44,26 +55,17 @@ export const routes: {
|
|
|
44
55
|
'post /api/signin': (typeof signin)[];
|
|
45
56
|
'post /api/signup': (typeof signup)[];
|
|
46
57
|
'post /api/reset-instructions': (typeof resetInstructions)[];
|
|
47
|
-
'post /api/reset-
|
|
58
|
+
'post /api/reset-confirm': (typeof resetConfirm)[];
|
|
48
59
|
'post /api/invite-instructions': (typeof inviteInstructions)[];
|
|
49
|
-
'post /api/invite-
|
|
60
|
+
'post /api/invite-confirm': (typeof inviteConfirm)[];
|
|
50
61
|
'delete /api/account/:uid': (typeof remove)[];
|
|
51
62
|
setup: typeof setup;
|
|
52
|
-
findUserFromProvider: typeof findUserFromProvider;
|
|
53
|
-
getStore: typeof getStore;
|
|
54
|
-
signinAndGetStore: typeof signinAndGetStore;
|
|
55
|
-
tokenCreate: typeof tokenCreate;
|
|
56
|
-
tokenParse: typeof tokenParse;
|
|
57
|
-
userCreate: typeof userCreate;
|
|
58
|
-
validatePassword: typeof validatePassword;
|
|
59
|
-
sendToken: typeof sendToken;
|
|
60
|
-
inviteOrResetConfirm: typeof inviteOrResetConfirm;
|
|
61
63
|
};
|
|
62
64
|
declare function store(req: any, res: any): Promise<void>;
|
|
63
65
|
declare function signout(req: any, res: any): void;
|
|
64
66
|
declare function signin(req: any, res: any): any;
|
|
65
67
|
declare function signup(req: any, res: any): Promise<void>;
|
|
66
68
|
declare function remove(req: any, res: any): Promise<void>;
|
|
67
|
-
declare function setup(middleware: any, _config: any): void;
|
|
69
|
+
declare function setup(middleware: any, _config: any, helpers?: {}): void;
|
|
68
70
|
export {};
|
|
69
71
|
//# sourceMappingURL=auth.api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"AAuKA,qEAeC;AAED,sEAsCC;AAED,gEAMC;AAED,iEAgBC;AAID,qGA0CC;AAED,4EAOC;AAED;;GAKC;AAID;;;;;;;;GAQG;AACH,8EANG;IAA0B,QAAQ,GAA1B,MAAM;IACY,SAAS,GAA3B,MAAM;IACY,OAAO,GAAzB,MAAM;CACd,kBAAQ,OAAO,GACL,OAAO,CAAC,MAAM,CAAC,CA0D3B;AAED,kFAiBC;AAED,mEAOC;AAED,+EAWC;AAED,6DAEC;AAED,kFAoBC;AAED,mEA4BC;AAED;;;;;;;;;;GAUG;AACH,0FARG;IAAsD,IAAI,EAAlD,OAAO,GAAG,QAAQ,GAAG,eAAe;IACpB,GAAG,EAAnB,MAAM;IACU,KAAK,EAArB,MAAM;IACU,SAAS,EAAzB,MAAM;IACa,YAAY;IACZ,eAAe;CAC1C,GAAU,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAC,CAAC,CAuCtE;AA1fD;;;;;;;;;;;EAaC;AAyED,0DAEC;AA6BD,mDAEC;AAnBD,iDAeC;AAzBD,2DAQC;AAuBD,2DAwBC;AAlID,0EAqEC"}
|
|
@@ -90,25 +90,39 @@ declare namespace _default {
|
|
|
90
90
|
export { _default_2 as default };
|
|
91
91
|
let _enum: string[];
|
|
92
92
|
export { _enum as enum };
|
|
93
|
+
let required_3: boolean;
|
|
94
|
+
export { required_3 as required };
|
|
93
95
|
}
|
|
94
96
|
let users: {
|
|
95
97
|
_id: {
|
|
96
98
|
model: string;
|
|
99
|
+
required: boolean;
|
|
97
100
|
};
|
|
98
101
|
role: {
|
|
99
102
|
type: string;
|
|
100
103
|
enum: string[];
|
|
104
|
+
required: boolean;
|
|
101
105
|
};
|
|
102
106
|
status: {
|
|
103
107
|
type: string;
|
|
104
|
-
|
|
108
|
+
default: string;
|
|
105
109
|
enum: string[];
|
|
110
|
+
required: boolean;
|
|
111
|
+
};
|
|
112
|
+
}[];
|
|
113
|
+
let invites: {
|
|
114
|
+
email: {
|
|
115
|
+
type: string;
|
|
116
|
+
required: boolean;
|
|
106
117
|
};
|
|
107
|
-
|
|
118
|
+
role: {
|
|
108
119
|
type: string;
|
|
120
|
+
enum: string[];
|
|
121
|
+
required: boolean;
|
|
109
122
|
};
|
|
110
123
|
inviteToken: {
|
|
111
124
|
type: string;
|
|
125
|
+
required: boolean;
|
|
112
126
|
};
|
|
113
127
|
}[];
|
|
114
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.d.ts","sourceRoot":"","sources":["../../../server/models/company.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"company.d.ts","sourceRoot":"","sources":["../../../server/models/company.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA+CgB,sCAEX;QACc;;;;;;;;;;;;;;;;;;;;;;;YAyBd"}
|
|
@@ -1,83 +1,80 @@
|
|
|
1
1
|
declare namespace _default {
|
|
2
2
|
namespace fields {
|
|
3
|
-
|
|
3
|
+
namespace avatar {
|
|
4
4
|
let type: string;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
namespace company {
|
|
7
7
|
let model: string;
|
|
8
8
|
let required: boolean;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
namespace email {
|
|
11
11
|
let type_1: string;
|
|
12
12
|
export { type_1 as type };
|
|
13
13
|
let required_1: boolean;
|
|
14
14
|
export { required_1 as required };
|
|
15
15
|
export let index: string;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
namespace isAdmin {
|
|
18
18
|
let type_2: string;
|
|
19
19
|
export { type_2 as type };
|
|
20
|
+
let _default: boolean;
|
|
21
|
+
export { _default as default };
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
namespace isInvited {
|
|
22
24
|
let type_3: string;
|
|
23
25
|
export { type_3 as type };
|
|
24
|
-
let required_2: boolean;
|
|
25
|
-
export { required_2 as required };
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
namespace firstName {
|
|
28
28
|
let type_4: string;
|
|
29
29
|
export { type_4 as type };
|
|
30
|
-
let
|
|
31
|
-
export {
|
|
30
|
+
let required_2: boolean;
|
|
31
|
+
export { required_2 as required };
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
namespace lastName {
|
|
34
34
|
let type_5: string;
|
|
35
35
|
export { type_5 as type };
|
|
36
|
-
let
|
|
37
|
-
export {
|
|
38
|
-
let _enum: string[];
|
|
39
|
-
export { _enum as enum };
|
|
36
|
+
let required_3: boolean;
|
|
37
|
+
export { required_3 as required };
|
|
40
38
|
}
|
|
41
|
-
|
|
39
|
+
namespace status {
|
|
42
40
|
let type_6: string;
|
|
43
41
|
export { type_6 as type };
|
|
42
|
+
let _default_1: string;
|
|
43
|
+
export { _default_1 as default };
|
|
44
|
+
let _enum: string[];
|
|
45
|
+
export { _enum as enum };
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
namespace stripeCustomer {
|
|
46
48
|
let type_7: string;
|
|
47
49
|
export { type_7 as type };
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
namespace stripeSubscription {
|
|
50
52
|
let type_8: string;
|
|
51
53
|
export { type_8 as type };
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
namespace stripeIntents {
|
|
56
|
+
let type_9: string;
|
|
57
|
+
export { type_9 as type };
|
|
58
|
+
}
|
|
59
|
+
namespace usedFreeTrial {
|
|
54
60
|
let type_10: string;
|
|
55
61
|
export { type_10 as type };
|
|
56
|
-
let
|
|
57
|
-
export {
|
|
58
|
-
let _enum_1: string[];
|
|
59
|
-
export { _enum_1 as enum };
|
|
62
|
+
let _default_2: boolean;
|
|
63
|
+
export { _default_2 as default };
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
export namespace usedFreeTrial {
|
|
65
|
+
namespace password {
|
|
63
66
|
let type_11: string;
|
|
64
67
|
export { type_11 as type };
|
|
65
|
-
let
|
|
66
|
-
export { _default_2 as default };
|
|
68
|
+
export let minLength: number;
|
|
67
69
|
}
|
|
68
|
-
|
|
70
|
+
namespace inviteToken {
|
|
69
71
|
let type_12: string;
|
|
70
72
|
export { type_12 as type };
|
|
71
|
-
export let minLength: number;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
namespace resetToken {
|
|
74
75
|
let type_13: string;
|
|
75
76
|
export { type_13 as type };
|
|
76
77
|
}
|
|
77
|
-
export namespace resetToken {
|
|
78
|
-
let type_14: string;
|
|
79
|
-
export { type_14 as type };
|
|
80
|
-
}
|
|
81
78
|
}
|
|
82
79
|
let findBL: string[];
|
|
83
80
|
let updateBL: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../server/models/user.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../server/models/user.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiDmB,gCAEd"}
|
package/types.ts
CHANGED
|
@@ -24,13 +24,13 @@ export type Config = InjectedConfig & {
|
|
|
24
24
|
|
|
25
25
|
export type User = {
|
|
26
26
|
_id?: string
|
|
27
|
+
email?: string
|
|
27
28
|
firstName?: string
|
|
28
29
|
lastName?: string
|
|
29
30
|
name?: string
|
|
30
31
|
avatar?: MonasteryImage
|
|
31
32
|
isAdmin?: boolean
|
|
32
33
|
isInvited?: boolean
|
|
33
|
-
type?: string
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export type Error = { title: string, detail: string }
|