nitro-web 0.0.29 → 0.0.30

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.
@@ -12,7 +12,6 @@ let config = {}
12
12
  const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
13
13
 
14
14
  export default {
15
-
16
15
  routes: {
17
16
  'get /api/store': ['store'],
18
17
  'get /api/signout': ['signout'],
@@ -22,336 +21,341 @@ export default {
22
21
  'post /api/reset-password': ['resetPassword'],
23
22
  'delete /api/account/:uid': ['isUser', 'remove'],
24
23
  },
24
+ setup: setup,
25
+ store: store,
26
+ signup: signup,
27
+ signin: signin,
28
+ signout: signout,
29
+ resetInstructions: resetInstructions,
30
+ resetPassword: resetPassword,
31
+ remove: remove,
32
+ }
25
33
 
26
- setup: function (middleware, _config) {
27
- const that = this
28
- global.passport = passport
29
-
30
- // Set config values
31
- config = { env: _config.env, masterPassword: _config.masterPassword }
32
- if (!config.env) throw new Error('Missing config value for: config.env')
33
-
34
- passport.use(
35
- new passportLocal.Strategy(
36
- { usernameField: 'email' },
37
- async (email, password, next) => {
38
- try {
39
- const user = await that._findUserFromProvider('email', { email, password })
40
- next(null, user)
41
- } catch (err) {
42
- next(err.message)
43
- }
34
+ function setup(middleware, _config) {
35
+ // Set config values
36
+ config = { env: _config.env, masterPassword: _config.masterPassword }
37
+ if (!config.env) throw new Error('Missing config value for: config.env')
38
+
39
+ passport.use(
40
+ new passportLocal.Strategy(
41
+ { usernameField: 'email' },
42
+ async (email, password, next) => {
43
+ try {
44
+ const user = await findUserFromProvider({ email }, password)
45
+ next(null, user)
46
+ } catch (err) {
47
+ next(err.message)
44
48
  }
45
- )
49
+ }
46
50
  )
51
+ )
47
52
 
48
- passport.use(
49
- new JwtStrategy(
50
- {
51
- jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
52
- secretOrKey: JWT_SECRET,
53
- },
54
- async (payload, done) => {
55
- try {
56
- const user = await that._findUserFromProvider('deserialize', { _id: payload._id })
57
- if (!user) return done(null, false)
58
- return done(null, user)
59
- } catch (err) {
60
- return done(err, false)
61
- }
53
+ passport.use(
54
+ new JwtStrategy(
55
+ {
56
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
57
+ secretOrKey: JWT_SECRET,
58
+ },
59
+ async (payload, done) => {
60
+ try {
61
+ const user = await findUserFromProvider({ _id: payload._id })
62
+ if (!user) return done(null, false)
63
+ return done(null, user)
64
+ } catch (err) {
65
+ return done(err, false)
62
66
  }
63
- )
67
+ }
64
68
  )
69
+ )
65
70
 
66
- middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
71
+ middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
67
72
 
68
- Object.assign(middleware, {
69
- blocked: function (req, res, next) {
70
- if (req.user && req.user.loginActive === false) {
71
- res.status(403).error('This user is not available.')
72
- } else {
73
- next()
74
- }
75
- },
76
- jwtAuth: function(req, res, next) {
77
- passport.authenticate('jwt', { session: false }, function(err, user) {
78
- if (user) req.user = user
79
- next()
80
- })(req, res, next)
81
- },
82
- passport: passport.initialize(),
83
- passportError: function (err, req, res, next) {
84
- if (!err) return next()
85
- res.error(err)
86
- },
87
- })
88
- },
89
-
90
- store: async function (req, res) {
91
- res.json(await this._getStore(req.user))
92
- },
93
-
94
- signup: async function (req, res) {
95
- try {
96
- let user = await this._userCreate(req.body)
97
- sendEmail({
98
- config: config,
99
- template: 'welcome',
100
- to: `${util.ucFirst(user.firstName)}<${user.email}>`,
101
- }).catch(console.error)
102
- res.send(await this._signinAndGetState(user, req.query.desktop))
103
- } catch (err) {
73
+ Object.assign(middleware, {
74
+ blocked: function (req, res, next) {
75
+ if (req.user && req.user.loginActive === false) {
76
+ res.status(403).error('This user is not available.')
77
+ } else {
78
+ next()
79
+ }
80
+ },
81
+ jwtAuth: function(req, res, next) {
82
+ passport.authenticate('jwt', { session: false }, function(err, user) {
83
+ if (user) req.user = user
84
+ next()
85
+ })(req, res, next)
86
+ },
87
+ passport: passport.initialize(),
88
+ passportError: function (err, req, res, next) {
89
+ if (!err) return next()
104
90
  res.error(err)
105
- }
106
- },
91
+ },
92
+ })
93
+ }
107
94
 
108
- signin: function (req, res) {
109
- if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
110
- if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
111
-
112
- passport.authenticate('local', { session: false }, async (err, user, info) => {
113
- if (err) return res.error(err)
114
- if (!user && info) return res.error('email', info.message)
115
- try {
116
- const response = await this._signinAndGetState(user, req.query.desktop)
117
- res.send(response)
118
- } catch (err) {
119
- res.error(err)
120
- }
121
- })(req, res)
122
- },
95
+ async function store(req, res) {
96
+ res.json(await getStore(req.user))
97
+ }
123
98
 
124
- signout: function (req, res) {
125
- res.json('{}')
126
- },
99
+ async function signup(req, res) {
100
+ try {
101
+ let user = await userCreate(req.body)
102
+ sendEmail({
103
+ config: config,
104
+ template: 'welcome',
105
+ to: `${util.ucFirst(user.firstName)}<${user.email}>`,
106
+ }).catch(console.error)
107
+ res.send(await signinAndGetState(user, req.query.desktop))
108
+ } catch (err) {
109
+ res.error(err)
110
+ }
111
+ }
127
112
 
128
- resetInstructions: async function (req, res) {
129
- try {
130
- let email = (req.body.email || '').trim().toLowerCase()
131
- if (!email || !util.isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
132
-
133
- let user = await db.user.findOne({ query: { email }, _privateData: true })
134
- if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
135
-
136
- let resetToken = await this._tokenCreate(user._id)
137
- await db.user.update({ query: { email }, $set: { resetToken }})
138
-
139
- res.json({})
140
- sendEmail({
141
- config: config,
142
- template: 'reset-password',
143
- to: `${util.ucFirst(user.firstName)}<${email}>`,
144
- data: {
145
- token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
146
- },
147
- }).catch(err => console.error('sendEmail(..) mailgun error', err))
148
- } catch (err) {
149
- res.error(err)
150
- }
151
- },
113
+ function signin(req, res) {
114
+ if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
115
+ if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
152
116
 
153
- resetPassword: async function (req, res) {
117
+ passport.authenticate('local', { session: false }, async (err, user, info) => {
118
+ if (err) return res.error(err)
119
+ if (!user && info) return res.error('email', info.message)
154
120
  try {
155
- const { token, password, password2 } = req.body
156
- const id = this._tokenParse(token)
157
- this._validatePassword(password, password2)
158
-
159
- let user = await db.user.findOne({ query: id, blacklist: ['-resetToken'], _privateData: true })
160
- if (!user || user.resetToken !== token) throw new Error('Sorry your email token is invalid or has already been used.')
161
-
162
- await db.user.update({
163
- query: user._id,
164
- data: {
165
- password: await (await import('bcrypt')).hash(password, 10),
166
- resetToken: '',
167
- },
168
- blacklist: ['-resetToken', '-password'],
169
- })
170
- res.send(await this._signinAndGetState({ ...user, resetToken: undefined }, req.query.desktop))
121
+ const response = await signinAndGetState(user, req.query.desktop)
122
+ res.send(response)
171
123
  } catch (err) {
172
124
  res.error(err)
173
125
  }
174
- },
126
+ })(req, res)
127
+ }
175
128
 
176
- remove: async function (req, res) {
177
- try {
178
- const uid = db.id(req.params.uid || 'badid')
129
+ function signout(req, res) {
130
+ res.json('{}')
131
+ }
179
132
 
180
- // Get companies owned by user
181
- const companyIdsOwned = (await db.company.find({
182
- query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
183
- project: { _id: 1 },
184
- })).map(o => o._id)
133
+ async function resetInstructions(req, res) {
134
+ try {
135
+ let email = (req.body.email || '').trim().toLowerCase()
136
+ if (!email || !util.isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
185
137
 
186
- // Check for active subscription first...
187
- if (req.user.stripeSubscription?.status == 'active') {
188
- throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
189
- }
190
-
191
- if (companyIdsOwned.length) {
192
- await db.transaction.remove({ query: { company: { $in: companyIdsOwned }}})
193
- await db.statement.remove({ query: { company: { $in: companyIdsOwned }}})
194
- await db.account.remove({ query: { company: { $in: companyIdsOwned }}})
195
- await db.document.remove({ query: { company: { $in: companyIdsOwned }}})
196
- await db.contact.remove({ query: { company: { $in: companyIdsOwned }}})
197
- await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
198
- await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
199
- }
200
- await db.user.remove({ query: { _id: uid }})
201
- // Logout now so that an error doesn't throw when naviating to /signout
202
- req.logout()
203
- res.send(`User: '${uid}' and companies: '${companyIdsOwned.join(', ')}' removed successfully`)
204
- } catch (err) {
205
- res.error(err)
206
- }
207
- },
138
+ let user = await db.user.findOne({ query: { email }, _privateData: true })
139
+ if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
208
140
 
209
- /* ---- Private fns ---------------- */
141
+ let resetToken = await tokenCreate(user._id)
142
+ await db.user.update({ query: { email }, $set: { resetToken }})
210
143
 
211
- _getStore: async function (user) {
212
- // Initial store
213
- return {
214
- user: user || undefined,
215
- }
216
- },
144
+ res.json({})
145
+ sendEmail({
146
+ config: config,
147
+ template: 'reset-password',
148
+ to: `${util.ucFirst(user.firstName)}<${email}>`,
149
+ data: {
150
+ token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
151
+ },
152
+ }).catch(err => console.error('sendEmail(..) mailgun error', err))
153
+ } catch (err) {
154
+ res.error(err)
155
+ }
156
+ }
217
157
 
218
- _signinAndGetState: async function (user, isDesktop) {
219
- if (user.loginActive === false) throw 'This user is not available.'
220
- user.desktop = isDesktop
158
+ async function resetPassword(req, res) {
159
+ try {
160
+ const { token, password, password2 } = req.body
161
+ const id = tokenParse(token)
162
+ await validatePassword(password, password2)
221
163
 
222
- const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
223
- const store = await this._getStore(user)
224
- return { ...store, jwt }
225
- },
164
+ let user = await db.user.findOne({ query: id, blacklist: ['-resetToken'], _privateData: true })
165
+ if (!user || user.resetToken !== token) throw new Error('Sorry your email token is invalid or has already been used.')
226
166
 
227
- _tokenCreate: function (id) {
228
- return new Promise((resolve) => {
229
- crypto.randomBytes(16, (err, buff) => {
230
- let hash = buff.toString('hex') // 32 chars
231
- resolve(`${hash}${id || ''}:${Date.now()}`)
232
- })
167
+ await db.user.update({
168
+ query: user._id,
169
+ data: {
170
+ password: await (await import('bcrypt')).hash(password, 10),
171
+ resetToken: '',
172
+ },
173
+ blacklist: ['-resetToken', '-password'],
233
174
  })
234
- },
175
+ res.send(await signinAndGetState({ ...user, resetToken: undefined }, req.query.desktop))
176
+ } catch (err) {
177
+ res.error(err)
178
+ }
179
+ }
235
180
 
236
- _tokenParse: function (token) {
237
- let split = (token || '').split(':')
238
- let hash = split[0].slice(0, 32)
239
- let userId = split[0].slice(32)
240
- let time = split[1]
241
- if (!hash || !userId || !time) {
242
- throw { title: 'error', detail: 'Sorry your code is invalid.' }
243
- } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
244
- throw { title: 'error', detail: 'Sorry your code has timed out.' }
245
- } else {
246
- return userId
247
- }
248
- },
181
+ async function remove(req, res) {
182
+ try {
183
+ const uid = db.id(req.params.uid || 'badid')
184
+
185
+ // Get companies owned by user
186
+ const companyIdsOwned = (await db.company.find({
187
+ query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
188
+ project: { _id: 1 },
189
+ })).map(o => o._id)
249
190
 
250
- _validatePassword: async function (password='', password2) {
251
- // let hasLowerChar = password.match(/[a-z]/)
252
- // let hasUpperChar = password.match(/[A-Z]/)
253
- // let hasNumber = password.match(/\d/)
254
- // let hasSymbol = password.match(/\W/)
255
- if (!password) {
256
- throw [{ title: 'password', detail: 'This field is required.' }]
257
- } else if (config.env !== 'development' && password.length < 8) {
258
- throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
259
- // } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
260
- // throw {
261
- // title: 'password',
262
- // detail: 'You need to include uppercase and lowercase letters, and a number'
263
- // }
264
- } else if (typeof password2 != 'undefined' && password !== password2) {
265
- throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
191
+ // Check for active subscription first...
192
+ if (req.user.stripeSubscription?.status == 'active') {
193
+ throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
266
194
  }
267
- },
195
+
196
+ if (companyIdsOwned.length) {
197
+ await db.transaction.remove({ query: { company: { $in: companyIdsOwned }}})
198
+ await db.statement.remove({ query: { company: { $in: companyIdsOwned }}})
199
+ await db.account.remove({ query: { company: { $in: companyIdsOwned }}})
200
+ await db.document.remove({ query: { company: { $in: companyIdsOwned }}})
201
+ await db.contact.remove({ query: { company: { $in: companyIdsOwned }}})
202
+ await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
203
+ await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
204
+ }
205
+ await db.user.remove({ query: { _id: uid }})
206
+ // Logout now so that an error doesn't throw when naviating to /signout
207
+ req.logout()
208
+ res.send(`User: '${uid}' and companies: '${companyIdsOwned.join(', ')}' removed successfully`)
209
+ } catch (err) {
210
+ res.error(err)
211
+ }
212
+ }
268
213
 
269
- _userCreate: async function ({ name, business, email, password }) {
270
- try {
271
- const options = { skipValidation: ['business.address', 'tax'], blacklist: ['-_id'] }
272
- const userId = db.id()
273
- const companyData = {
274
- _id: db.id(),
275
- business: business,
276
- email: email,
277
- users: [{ _id: userId, role: 'owner', status: 'active' }],
278
- }
279
- const userData = {
280
- _id: userId,
281
- company: companyData._id,
282
- email: email,
283
- firstName: util.fullNameSplit(name)[0],
284
- lastName: util.fullNameSplit(name)[1],
285
- password: await (await import('bcrypt')).hash(password || 'temp', 10),
286
- }
214
+ /* ---- Private fns ---------------- */
287
215
 
288
- // First validate the data so we don't have to create a transaction
289
- const results = await Promise.allSettled([
290
- db.user.validate(userData, options),
291
- db.company.validate(companyData, options),
292
- this._validatePassword(password),
293
- ])
216
+ async function getStore(user) {
217
+ // Initial store
218
+ return {
219
+ user: user || undefined,
220
+ }
221
+ }
294
222
 
295
- // Throw all the errors from at once
296
- const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
297
- if (util.isArray(o.reason)) acc.push(...o.reason)
298
- else throw o.reason
299
- return acc
300
- }, [])
301
- if (errors.length) throw errors
223
+ export async function signinAndGetState(user, isDesktop) {
224
+ if (user.loginActive === false) throw 'This user is not available.'
225
+ user.desktop = isDesktop
302
226
 
303
- // Insert company & user
304
- await db.user.insert({ data: userData, ...options })
305
- await db.company.insert({ data: companyData, ...options })
227
+ const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
228
+ const store = await getStore(user)
229
+ return { ...store, jwt }
230
+ }
306
231
 
307
- // Return the user
308
- return await this._findUserFromProvider('deserialize', { _id: userId })
232
+ function tokenCreate(id) {
233
+ return new Promise((resolve) => {
234
+ crypto.randomBytes(16, (err, buff) => {
235
+ let hash = buff.toString('hex') // 32 chars
236
+ resolve(`${hash}${id || ''}:${Date.now()}`)
237
+ })
238
+ })
239
+ }
309
240
 
310
- } catch (err) {
311
- if (!util.isArray(err)) throw err
312
- throw err.map((o) => {
313
- if (o.title == 'firstName') o.title = 'name'
314
- return o
315
- })
241
+ function tokenParse(token) {
242
+ let split = (token || '').split(':')
243
+ let hash = split[0].slice(0, 32)
244
+ let userId = split[0].slice(32)
245
+ let time = split[1]
246
+ if (!hash || !userId || !time) {
247
+ throw { title: 'error', detail: 'Sorry your code is invalid.' }
248
+ } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
249
+ throw { title: 'error', detail: 'Sorry your code has timed out.' }
250
+ } else {
251
+ return userId
252
+ }
253
+ }
254
+
255
+ async function validatePassword(password='', password2) {
256
+ // let hasLowerChar = password.match(/[a-z]/)
257
+ // let hasUpperChar = password.match(/[A-Z]/)
258
+ // let hasNumber = password.match(/\d/)
259
+ // let hasSymbol = password.match(/\W/)
260
+ if (!password) {
261
+ throw [{ title: 'password', detail: 'This field is required.' }]
262
+ } else if (config.env !== 'development' && password.length < 8) {
263
+ throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
264
+ // } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
265
+ // throw {
266
+ // title: 'password',
267
+ // detail: 'You need to include uppercase and lowercase letters, and a number'
268
+ // }
269
+ } else if (typeof password2 != 'undefined' && password !== password2) {
270
+ throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
271
+ }
272
+ }
273
+
274
+ export async function userCreate({ name, businessName, email, password }) {
275
+ try {
276
+ const options = { blacklist: ['-_id'] }
277
+ const userId = db.id()
278
+ const companyData = {
279
+ _id: db.id(),
280
+ business: { name: businessName },
281
+ users: [{ _id: userId, role: 'owner', status: 'active' }],
282
+ }
283
+ const userData = {
284
+ _id: userId,
285
+ company: companyData._id,
286
+ email: email,
287
+ firstName: util.fullNameSplit(name)[0],
288
+ lastName: util.fullNameSplit(name)[1],
289
+ password: password ? await (await import('bcrypt')).hash(password, 10) : undefined,
316
290
  }
317
- },
318
291
 
319
- _findUserFromProvider: async function (type, { _id, email, password }) {
320
- /**
321
- * Find user for state (and verify password if signing in with email)
322
- * @param {string} type - 'deserialize' or 'email' (jwt, google, etc)
323
- * @param {string} data - req.data
324
- */
325
- const user = await db.user.findOne({
326
- query: type == 'email' ? { email } : _id,
327
- blacklist: ['-password'],
328
- populate: db.user.loginPopulate(),
292
+ // First validate the data so we don't have to create a transaction
293
+ const results = await Promise.allSettled([
294
+ db.user.validate(userData, options),
295
+ db.company.validate(companyData, options),
296
+ typeof password === 'undefined' ? Promise.resolve() : validatePassword(password),
297
+ ])
298
+
299
+ // Throw all the errors from at once
300
+ const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
301
+ if (util.isArray(o.reason)) acc.push(...o.reason)
302
+ else throw o.reason
303
+ return acc
304
+ }, [])
305
+ if (errors.length) throw errors
306
+
307
+ // Insert company & user
308
+ await db.user.insert({ data: userData, ...options })
309
+ await db.company.insert({ data: companyData, ...options })
310
+
311
+ // Return the user
312
+ return await findUserFromProvider({ _id: userId })
313
+
314
+ } catch (err) {
315
+ if (!util.isArray(err)) throw err
316
+ throw err.map((o) => {
317
+ if (o.title == 'firstName') o.title = 'name'
318
+ return o
319
+ })
320
+ }
321
+ }
322
+
323
+ export async function findUserFromProvider(query, passwordToTest) {
324
+ /**
325
+ * Find user for state (and verify password if signing in with email)
326
+ * @param {object} query - e.g. { email: 'test@test.com' }
327
+ * @param {string} <passwordToTest> - password to test
328
+ */
329
+ const testPassword = arguments.length > 1
330
+ const user = await db.user.findOne({
331
+ query: query,
332
+ blacklist: ['-password'],
333
+ populate: db.user.loginPopulate(),
334
+ _privateData: true,
335
+ })
336
+ if (user?.company) {
337
+ user.company = await db.company.findOne({
338
+ query: user.company,
339
+ populate: db.company.loginPopulate(),
329
340
  _privateData: true,
330
341
  })
331
- if (user?.company) {
332
- user.company = await db.company.findOne({
333
- query: user.company,
334
- populate: db.company.loginPopulate(),
335
- _privateData: true,
336
- })
337
- }
338
- if (!user) {
339
- throw new Error(type == 'email' ? 'Email or password is incorrect.' : 'Session user is invalid.')
340
- } else if (!user.company) {
341
- throw new Error('The current company is no longer associated with this user')
342
- } else if (user.company.status != 'active') {
343
- throw new Error('This user is not associated with an active company')
344
- } else {
345
- if (type == 'email') {
346
- const match = await (await import('bcrypt')).compare(password, user.password || 'no-pass')
347
- if (!match && !(config.masterPassword && password == config.masterPassword)) {
348
- throw new Error('Email or password is incorrect.')
349
- }
342
+ }
343
+ if (!user) {
344
+ throw new Error(testPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
345
+ } else if (!user.company) {
346
+ throw new Error('The current company is no longer associated with this user')
347
+ } else if (user.company.status != 'active') {
348
+ throw new Error('This user is not associated with an active company')
349
+ } else {
350
+ if (testPassword) {
351
+ const match = user.password ? await (await import('bcrypt')).compare(passwordToTest, user.password) : false
352
+ if (!match && !(config.masterPassword && passwordToTest == config.masterPassword)) {
353
+ throw new Error('Email or password is incorrect.')
350
354
  }
351
- // Successful return user
352
- delete user.password
353
- return user
354
355
  }
355
- },
356
-
356
+ // Successful return user
357
+ delete user.password
358
+ return user
359
+ }
357
360
  }
361
+
@@ -35,8 +35,8 @@ export function Signup() {
35
35
  <Field name="name" placeholder="E.g. Bruce Wayne" state={state} onChange={onChange.bind(setState)} />
36
36
  </div>
37
37
  <div>
38
- <label for="business.name">Company Name</label>
39
- <Field name="business.name" placeholder="E.g. Wayne Enterprises" state={state} onChange={onChange.bind(setState)} />
38
+ <label for="businessName">Company Name</label>
39
+ <Field name="businessName" placeholder="E.g. Wayne Enterprises" state={state} onChange={onChange.bind(setState)} />
40
40
  </div>
41
41
  </div>
42
42
  <div>
@@ -13,256 +13,258 @@ export default {
13
13
  'post /api/stripe/create-billing-portal-session': ['isUser', 'billingPortalSessionCreate'],
14
14
  'get /api/stripe/upcoming-invoices': ['isUser', 'upcomingInvoicesFind'],
15
15
  },
16
+ setup: setup,
17
+ stripeWebhook: stripeWebhook,
18
+ billingPortalSessionCreate: billingPortalSessionCreate,
19
+ upcomingInvoicesFind: upcomingInvoicesFind,
20
+ }
16
21
 
17
- setup: function (middleware, _config) {
18
- // Set config values
19
- config = {
20
- env: _config.env,
21
- clientUrl: _config.clientUrl,
22
- stripeSecretKey: _config.stripeSecretKey,
23
- stripeWebhookSecret: _config.stripeWebhookSecret,
24
- }
25
- for (let key in config) {
26
- if (!config[key]) {
27
- throw new Error(`Missing config value for stripe.api.js: ${key}`)
28
- }
29
- }
30
- stripe = new Stripe(config.stripeSecretKey)
31
- },
32
-
33
- stripeWebhook: async function (req, res) {
34
- try {
35
- var event = config.env == 'development' ? req.body : stripe.webhooks.constructEvent(
36
- req.rawBody,
37
- req.rawHeaders['stripe-signature'],
38
- config.stripeWebhookSecret
39
- )
40
- } catch (err) {
41
- if (err && err.message) console.log(err.message)
42
- else console.log(err)
43
- return res.error(err)
22
+ function setup(middleware, _config) {
23
+ // Set config values
24
+ config = {
25
+ env: _config.env,
26
+ clientUrl: _config.clientUrl,
27
+ stripeSecretKey: _config.stripeSecretKey,
28
+ stripeWebhookSecret: _config.stripeWebhookSecret,
29
+ }
30
+ for (let key in config) {
31
+ if (!config[key]) {
32
+ throw new Error(`Missing config value for stripe.api.js: ${key}`)
44
33
  }
34
+ }
35
+ stripe = new Stripe(config.stripeSecretKey)
36
+ }
45
37
 
46
- if (!event.data || !event.data.object) {
47
- return res.status(400).send(`Missing webhook data: ${event}.`)
48
- }
38
+ async function stripeWebhook(req, res) {
39
+ try {
40
+ var event = config.env == 'development' ? req.body : stripe.webhooks.constructEvent(
41
+ req.rawBody,
42
+ req.rawHeaders['stripe-signature'],
43
+ config.stripeWebhookSecret
44
+ )
45
+ } catch (err) {
46
+ if (err && err.message) console.log(err.message)
47
+ else console.log(err)
48
+ return res.error(err)
49
+ }
49
50
 
50
- // useful for cleaning failed webhooks
51
- if (req.query.success) return true
52
- // console.log('event.type: ', event.type)
51
+ if (!event.data || !event.data.object) {
52
+ return res.status(400).send(`Missing webhook data: ${event}.`)
53
+ }
53
54
 
54
- // Stripe cannot guarantee event order
55
- switch (event.type) {
56
- case 'customer.subscription.created':
57
- case 'customer.subscription.updated':
58
- case 'customer.subscription.deleted':
59
- // Subscriptions can be renewed which resurrects cancelled subscriptions.. ignore this
60
- console.log('Event: ' + event.type)
61
- this._webhookSubUpdated(req, res, event)
62
- break
63
- case 'customer.created': // customer created by subscribing
64
- case 'customer.updated': // payment method changes
65
- console.log('Event: ' + event.type)
66
- this._webhookCustomerCreatedUpdated(req, res, event)
67
- break
68
- default:
69
- res.status(400).send(`Unsupported type: ${event}.`)
70
- break
71
- }
72
- },
55
+ // useful for cleaning failed webhooks
56
+ if (req.query.success) return true
57
+ // console.log('event.type: ', event.type)
73
58
 
74
- billingPortalSessionCreate: async function (req, res) {
75
- try {
76
- if (!req.user.stripeCustomer?.id) {
77
- throw new Error('No stripe customer found for the user')
78
- }
79
- const session = await stripe.billingPortal.sessions.create({
80
- customer: req.user.stripeCustomer.id,
81
- return_url: config.clientUrl + '/subscriptions',
82
- })
83
- res.json(session.url)
84
- } catch (err) {
85
- this._error(req, res, err)
86
- }
87
- },
59
+ // Stripe cannot guarantee event order
60
+ switch (event.type) {
61
+ case 'customer.subscription.created':
62
+ case 'customer.subscription.updated':
63
+ case 'customer.subscription.deleted':
64
+ // Subscriptions can be renewed which resurrects cancelled subscriptions.. ignore this
65
+ console.log('Event: ' + event.type)
66
+ webhookSubUpdated(req, res, event)
67
+ break
68
+ case 'customer.created': // customer created by subscribing
69
+ case 'customer.updated': // payment method changes
70
+ console.log('Event: ' + event.type)
71
+ webhookCustomerCreatedUpdated(req, res, event)
72
+ break
73
+ default:
74
+ res.status(400).send(`Unsupported type: ${event}.`)
75
+ break
76
+ }
77
+ }
88
78
 
89
- upcomingInvoicesFind: async function (req, res) {
90
- try {
91
- if (!req.user.stripeCustomer?.id) return res.json({})
92
- const nextInvoice = await stripe.invoices.retrieveUpcoming({
93
- customer: req.user.stripeCustomer.id,
94
- })
95
- res.json(nextInvoice)
96
- } catch (err) {
97
- if (err.code == 'invoice_upcoming_none') return res.json({})
98
- this._error(req, res, err)
79
+ async function billingPortalSessionCreate(req, res) {
80
+ try {
81
+ if (!req.user.stripeCustomer?.id) {
82
+ throw new Error('No stripe customer found for the user')
99
83
  }
100
- },
101
-
102
- /* Private webhook actions */
84
+ const session = await stripe.billingPortal.sessions.create({
85
+ customer: req.user.stripeCustomer.id,
86
+ return_url: config.clientUrl + '/subscriptions',
87
+ })
88
+ res.json(session.url)
89
+ } catch (err) {
90
+ error(req, res, err)
91
+ }
92
+ }
103
93
 
104
- _webhookCustomerCreatedUpdated: async function (req, res, event) {
105
- try {
106
- const customer = event.data.object
107
- const user = await this._getUserFromEvent(event)
108
- const customerExpanded = await stripe.customers.retrieve(
109
- customer.id,
110
- { expand: ['invoice_settings.default_payment_method'] }
111
- )
112
- await db.user.update({
113
- query: user._id,
114
- data: { stripeCustomer: customerExpanded },
115
- blacklist: ['-stripeCustomer'],
116
- })
117
- res.json({})
118
- } catch (err) {
119
- console.log(err)
120
- this._error(req, res, err)
121
- }
122
- },
94
+ async function upcomingInvoicesFind(req, res) {
95
+ try {
96
+ if (!req.user.stripeCustomer?.id) return res.json({})
97
+ const nextInvoice = await stripe.invoices.retrieveUpcoming({
98
+ customer: req.user.stripeCustomer.id,
99
+ })
100
+ res.json(nextInvoice)
101
+ } catch (err) {
102
+ if (err.code == 'invoice_upcoming_none') return res.json({})
103
+ error(req, res, err)
104
+ }
105
+ }
123
106
 
124
- _webhookSubUpdated: async function (req, res, event) {
125
- // Update the subscription on the company
126
- try {
127
- const subData = event.data.object
128
- // webhook from deleting a company?
129
- if (subData.cancellation_details.comment == 'company deleted') {
130
- return res.json({})
131
- }
107
+ /* Private webhook actions */
132
108
 
133
- const user = await this._getUserFromEvent(event)
134
- if (!user.company) {
135
- throw new Error(`Subscription user has no company to update the subscription (${subData.id}) onto`)
136
- }
109
+ async function webhookCustomerCreatedUpdated(req, res, event) {
110
+ try {
111
+ const customer = event.data.object
112
+ const user = await getUserFromEvent(event)
113
+ const customerExpanded = await stripe.customers.retrieve(
114
+ customer.id,
115
+ { expand: ['invoice_settings.default_payment_method'] }
116
+ )
117
+ await db.user.update({
118
+ query: user._id,
119
+ data: { stripeCustomer: customerExpanded },
120
+ blacklist: ['-stripeCustomer'],
121
+ })
122
+ res.json({})
123
+ } catch (err) {
124
+ console.log(err)
125
+ error(req, res, err)
126
+ }
127
+ }
137
128
 
138
- // Ignoring incomplete subscriptions
139
- if (subData.status.match(/incomplete/)) {
140
- return res.json({})
141
- // Ignoring subscriptions without companyId (e.g. manual subscriptions)
142
- } else if (!subData.metadata.companyId) {
143
- return res.json({ ignoringManualSubscriptions: true })
144
- // Ignoring old subscriptions
145
- } else if (subData.created < (user.company.stripeSubscription?.created || 0)) {
146
- return res.json({ ignoringOldSubscriptions: true })
147
- }
148
-
149
- // Update company with the updated subscription and users
150
- const sub = await stripe.subscriptions.retrieve(
151
- subData.id,
152
- { expand: ['latest_invoice.payment_intent'] }
153
- )
154
- await db.company.update({
155
- query: user.company._id,
156
- data: { stripeSubscription: sub, users: user.company.users },
157
- blacklist: ['-stripeSubscription', '-users'],
158
- })
159
-
160
- res.json({})
161
- } catch (err) {
162
- console.error(err)
163
- this._error(req, res, err)
129
+ async function webhookSubUpdated(req, res, event) {
130
+ // Update the subscription on the company
131
+ try {
132
+ const subData = event.data.object
133
+ // webhook from deleting a company?
134
+ if (subData.cancellation_details.comment == 'company deleted') {
135
+ return res.json({})
164
136
  }
165
- },
166
-
167
- /* Private */
168
137
 
169
- _getUserFromEvent: async function (event) {
170
- // User retreived from the event's customer.
171
- // The customer is created before the paymentIntent and subscriptionIntent is set up
172
- let object = event.data.object
173
- let customerId = object.object == 'customer'? object.id : object.customer
174
- if (customerId) {
175
- var user = await db.user.findOne({
176
- query: { 'stripeCustomer.id': customerId },
177
- populate: db.user.populate({}),
178
- blacklist: false, // ['-company.users.inviteToken'],
179
- })
138
+ const user = await getUserFromEvent(event)
139
+ if (!user.company) {
140
+ throw new Error(`Subscription user has no company to update the subscription (${subData.id}) onto`)
180
141
  }
181
- if (!user) {
182
- await db.log.insert({ data: {
183
- date: Date.now(),
184
- event: event.type,
185
- message: `Cannot find user with id: ${customerId}.`,
186
- }})
187
- throw new Error(`Cannot find user with id: ${customerId}.`)
188
- }
189
- // populate company owner with user data (handy for _addSubscriptionBillingChange)
190
- if (user.company?.users) {
191
- user.company.users = user.company.users.map(o => {
192
- if (o.role == 'owner' && o._id.toString() == user._id.toString()) {
193
- o.firstName = user.firstName
194
- o.name = user.name
195
- o.email = user.email
196
- }
197
- return o
198
- })
142
+
143
+ // Ignoring incomplete subscriptions
144
+ if (subData.status.match(/incomplete/)) {
145
+ return res.json({})
146
+ // Ignoring subscriptions without companyId (e.g. manual subscriptions)
147
+ } else if (!subData.metadata.companyId) {
148
+ return res.json({ ignoringManualSubscriptions: true })
149
+ // Ignoring old subscriptions
150
+ } else if (subData.created < (user.company.stripeSubscription?.created || 0)) {
151
+ return res.json({ ignoringOldSubscriptions: true })
199
152
  }
200
- return user
201
- },
153
+
154
+ // Update company with the updated subscription and users
155
+ const sub = await stripe.subscriptions.retrieve(
156
+ subData.id,
157
+ { expand: ['latest_invoice.payment_intent'] }
158
+ )
159
+ await db.company.update({
160
+ query: user.company._id,
161
+ data: { stripeSubscription: sub, users: user.company.users },
162
+ blacklist: ['-stripeSubscription', '-users'],
163
+ })
164
+
165
+ res.json({})
166
+ } catch (err) {
167
+ console.error(err)
168
+ error(req, res, err)
169
+ }
170
+ }
202
171
 
203
- _getProducts: async function () {
204
- /**
205
- * Returns all products and caches it on the app
206
- * @returns {Array} products
207
- */
208
- try {
209
- if (stripeProducts) return stripeProducts
210
- if (!config.stripeSecretKey) {
211
- stripeProducts = []
212
- throw new Error('Missing process.env.stripeSecretKey for retrieving products')
172
+ async function getUserFromEvent(event) {
173
+ // User retreived from the event's customer.
174
+ // The customer is created before the paymentIntent and subscriptionIntent is set up
175
+ let object = event.data.object
176
+ let customerId = object.object == 'customer'? object.id : object.customer
177
+ if (customerId) {
178
+ var user = await db.user.findOne({
179
+ query: { 'stripeCustomer.id': customerId },
180
+ populate: db.user.populate({}),
181
+ blacklist: false, // ['-company.users.inviteToken'],
182
+ })
183
+ }
184
+ if (!user) {
185
+ await db.log.insert({ data: {
186
+ date: Date.now(),
187
+ event: event.type,
188
+ message: `Cannot find user with id: ${customerId}.`,
189
+ }})
190
+ throw new Error(`Cannot find user with id: ${customerId}.`)
191
+ }
192
+ // populate company owner with user data (handy for _addSubscriptionBillingChange)
193
+ if (user.company?.users) {
194
+ user.company.users = user.company.users.map(o => {
195
+ if (o.role == 'owner' && o._id.toString() == user._id.toString()) {
196
+ o.firstName = user.firstName
197
+ o.name = user.name
198
+ o.email = user.email
213
199
  }
200
+ return o
201
+ })
202
+ }
203
+ return user
204
+ }
214
205
 
215
- let products = (await stripe.products.list({ limit: 100, active: true })).data
216
- let prices = (await stripe.prices.list({ limit: 100, active: true, expand: ['data.tiers'] })).data
217
-
218
- return (stripeProducts = products.map((product) => ({
219
- // remove default_price when new pricing is ready
220
- ...util.pick(product, ['id', 'created', 'default_price', 'description', 'name', 'metadata']),
221
- type: product.name.match(/housing/i) ? 'project' : 'subscription', // overwriting, was 'service'
222
- prices: prices
223
- .filter((price) => price.product == product.id)
224
- .map((price) => ({
225
- ...util.pick(price, ['id', 'product', 'nickname', 'recurring', 'unit_amount', 'tiers', 'tiers_mode']),
226
- interval: price.recurring?.interval, // 'year', 'month', undefined
227
- })),
228
- })))
229
- } catch (err) {
230
- console.error(new Error(err)) // when stripe throws errors, the callstack is missing.
231
- return []
206
+ export async function getProducts() {
207
+ /**
208
+ * Returns all products and caches it on the app
209
+ * @returns {Array} products
210
+ */
211
+ try {
212
+ if (stripeProducts) return stripeProducts
213
+ if (!config.stripeSecretKey) {
214
+ stripeProducts = []
215
+ throw new Error('Missing process.env.stripeSecretKey for retrieving products')
232
216
  }
233
- },
234
217
 
235
- _createOrUpdateCustomer: async function (user, paymentMethod=null) {
236
- /**
237
- * Creates or updates a stripe customer and saves it to the user
238
- * @param {Object} user - user
239
- * @param {String} paymentMethod - stripe payment method id to save to the customer
240
- * @called before paymentIntent and subscriptionIntent, and after completion with paymentMethod
241
- * @returns mutates user
242
- */
243
- const data = {
244
- email: user.email,
245
- name: user.name,
246
- address: { country: 'NZ' },
247
- ...(!paymentMethod ? {} : { invoice_settings: { default_payment_method: paymentMethod }}),
248
- expand: ['invoice_settings.default_payment_method', 'tax'], // expands card object
249
- }
250
-
251
- if (user.stripeCustomer) var customer = await stripe.customers.update(user.stripeCustomer.id, data)
252
- else customer = await stripe.customers.create({ ...data })
218
+ let products = (await stripe.products.list({ limit: 100, active: true })).data
219
+ let prices = (await stripe.prices.list({ limit: 100, active: true, expand: ['data.tiers'] })).data
253
220
 
254
- user.stripeCustomer = customer
255
- await db.user.update({
256
- query: user._id,
257
- data: { stripeCustomer: customer },
258
- blacklist: ['-stripeCustomer'],
259
- })
260
- },
221
+ return (stripeProducts = products.map((product) => ({
222
+ // remove default_price when new pricing is ready
223
+ ...util.pick(product, ['id', 'created', 'default_price', 'description', 'name', 'metadata']),
224
+ type: product.name.match(/housing/i) ? 'project' : 'subscription', // overwriting, was 'service'
225
+ prices: prices
226
+ .filter((price) => price.product == product.id)
227
+ .map((price) => ({
228
+ ...util.pick(price, ['id', 'product', 'nickname', 'recurring', 'unit_amount', 'tiers', 'tiers_mode']),
229
+ interval: price.recurring?.interval, // 'year', 'month', undefined
230
+ })),
231
+ })))
232
+ } catch (err) {
233
+ console.error(new Error(err)) // when stripe throws errors, the callstack is missing.
234
+ return []
235
+ }
236
+ }
261
237
 
262
- _error: async function (req, res, err) {
263
- if (err && err.response && err.response.body) console.log(err.response.body)
264
- if (util.isString(err) && err.match(/Cannot find company with id/)) {
265
- res.json({ user: 'no company found' })
266
- } else res.error(err)
267
- },
238
+ // async function createOrUpdateCustomer(user, paymentMethod=null) {
239
+ // /**
240
+ // * Creates or updates a stripe customer and saves it to the user
241
+ // * @param {Object} user - user
242
+ // * @param {String} paymentMethod - stripe payment method id to save to the customer
243
+ // * @called before paymentIntent and subscriptionIntent, and after completion with paymentMethod
244
+ // * @returns mutates user
245
+ // */
246
+ // const data = {
247
+ // email: user.email,
248
+ // name: user.name,
249
+ // address: { country: 'NZ' },
250
+ // ...(!paymentMethod ? {} : { invoice_settings: { default_payment_method: paymentMethod }}),
251
+ // expand: ['invoice_settings.default_payment_method', 'tax'], // expands card object
252
+ // }
253
+
254
+ // if (user.stripeCustomer) var customer = await stripe.customers.update(user.stripeCustomer.id, data)
255
+ // else customer = await stripe.customers.create({ ...data })
256
+
257
+ // user.stripeCustomer = customer
258
+ // await db.user.update({
259
+ // query: user._id,
260
+ // data: { stripeCustomer: customer },
261
+ // blacklist: ['-stripeCustomer'],
262
+ // })
263
+ // }
264
+
265
+ async function error(req, res, err) {
266
+ if (err && err.response && err.response.body) console.log(err.response.body)
267
+ if (util.isString(err) && err.match(/Cannot find company with id/)) {
268
+ res.json({ user: 'no company found' })
269
+ } else res.error(err)
268
270
  }
@@ -2,54 +2,54 @@
2
2
  import db from 'monastery'
3
3
 
4
4
  export default {
5
-
6
5
  routes: {
7
6
  'put /api/company/:cid': ['isCompanyUser', 'update'],
8
7
  'put /api/user/:uid': ['isUser', 'updateUser'],
9
8
  },
9
+ update: update,
10
+ updateUser: updateUser,
11
+ }
10
12
 
11
- update: async function(req, res) {
12
- try {
13
- const update = await db.company.update({
14
- query: req.params.cid,
15
- data: req.body,
16
- files: req.query.files ? req.files : undefined,
17
- })
18
- if (!update) {
19
- throw new Error('Coudln\'t find the company to update')
20
- }
21
- const company = await db.company.findOne({
22
- query: req.params.cid,
23
- populate: db.company.loginPopulate(),
24
- _privateData: true,
25
- })
26
- res.json(company)
27
-
28
- } catch (errs) {
29
- res.error(errs)
13
+ async function update(req, res) {
14
+ try {
15
+ const update = await db.company.update({
16
+ query: req.params.cid,
17
+ data: req.body,
18
+ files: req.query.files ? req.files : undefined,
19
+ })
20
+ if (!update) {
21
+ throw new Error('Coudln\'t find the company to update')
30
22
  }
31
- },
23
+ const company = await db.company.findOne({
24
+ query: req.params.cid,
25
+ populate: db.company.loginPopulate(),
26
+ _privateData: true,
27
+ })
28
+ res.json(company)
32
29
 
33
- updateUser: async function(req, res) {
34
- try {
35
- const update = await db.user.update({
36
- query: req.params.uid,
37
- data: req.body,
38
- files: req.query.files ? req.files : undefined,
39
- })
40
- if (!update) {
41
- throw new Error('Coudln\'t find the user to update')
42
- }
43
- const user = await db.user.findOne({
44
- query: req.params.uid,
45
- _privateData: true,
46
- blacklist: ['company'], // don't return the company id
47
- })
48
- res.json(user)
30
+ } catch (errs) {
31
+ res.error(errs)
32
+ }
33
+ }
49
34
 
50
- } catch (errs) {
51
- res.error(errs)
35
+ async function updateUser(req, res) {
36
+ try {
37
+ const update = await db.user.update({
38
+ query: req.params.uid,
39
+ data: req.body,
40
+ files: req.query.files ? req.files : undefined,
41
+ })
42
+ if (!update) {
43
+ throw new Error('Coudln\'t find the user to update')
52
44
  }
53
- },
45
+ const user = await db.user.findOne({
46
+ query: req.params.uid,
47
+ _privateData: true,
48
+ blacklist: ['company'], // don't return the company id
49
+ })
50
+ res.json(user)
54
51
 
52
+ } catch (errs) {
53
+ res.error(errs)
54
+ }
55
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
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,6 @@ export { setupRouter } from './router.js'
19
19
  export { sendEmail } from './email/index.js'
20
20
 
21
21
  // Export api default controllers
22
- export { default as auth } from '../components/auth/auth.api.js'
22
+ export { default as auth, findUserFromProvider, signinAndGetState, userCreate } from '../components/auth/auth.api.js'
23
23
  export { default as settings } from '../components/settings/settings.api.js'
24
24
  export { default as stripe } from '../components/billing/stripe.api.js'
@@ -9,7 +9,7 @@ export default {
9
9
  email: { type: 'email', required: true, index: 'unique' },
10
10
  firstName: { type: 'string', required: true },
11
11
  lastName: { type: 'string' },
12
- password: { type: 'string', minLength: 6, required: true },
12
+ password: { type: 'string', minLength: 6 },
13
13
  resetToken: { type: 'string' },
14
14
  status: { type: 'string', default: 'active', enum: ['active', 'deleted', 'inactive'] },
15
15
  stripeCustomer: { type: 'any' },