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.
- package/components/auth/auth.api.js +296 -292
- package/components/auth/signup.tsx +2 -2
- package/components/billing/stripe.api.js +231 -229
- package/components/settings/settings.api.js +40 -40
- package/package.json +1 -1
- package/server/index.js +1 -1
- package/server/models/user.js +1 -1
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
71
|
+
middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
156
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
129
|
+
function signout(req, res) {
|
|
130
|
+
res.json('{}')
|
|
131
|
+
}
|
|
179
132
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
141
|
+
let resetToken = await tokenCreate(user._id)
|
|
142
|
+
await db.user.update({ query: { email }, $set: { resetToken }})
|
|
210
143
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
216
|
+
async function getStore(user) {
|
|
217
|
+
// Initial store
|
|
218
|
+
return {
|
|
219
|
+
user: user || undefined,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
294
222
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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="
|
|
39
|
-
<Field name="
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
if (!event.data || !event.data.object) {
|
|
52
|
+
return res.status(400).send(`Missing webhook data: ${event}.`)
|
|
53
|
+
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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.
|
|
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'
|
package/server/models/user.js
CHANGED
|
@@ -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
|
|
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' },
|