nitro-web 0.0.1

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.
Files changed (152) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.json +86 -0
  3. package/_example/.env-example +16 -0
  4. package/_example/client/config.ts +5 -0
  5. package/_example/client/css/index.css +35 -0
  6. package/_example/client/fonts/Roboto-Bold.ttf +0 -0
  7. package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
  8. package/_example/client/fonts/Roboto-Italic.ttf +0 -0
  9. package/_example/client/fonts/Roboto-Medium.ttf +0 -0
  10. package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
  11. package/_example/client/fonts/Roboto-Regular.ttf +0 -0
  12. package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
  13. package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
  14. package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
  15. package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
  16. package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
  17. package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
  18. package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
  19. package/_example/client/imgs/android-chrome-512x512.png +0 -0
  20. package/_example/client/imgs/favicon.png +0 -0
  21. package/_example/client/imgs/icons/calendar.svg +3 -0
  22. package/_example/client/imgs/icons/email.svg +6 -0
  23. package/_example/client/imgs/icons/eye-open.svg +4 -0
  24. package/_example/client/imgs/icons/eye.svg +5 -0
  25. package/_example/client/imgs/icons/filter.svg +7 -0
  26. package/_example/client/imgs/icons/left-circle.svg +3 -0
  27. package/_example/client/imgs/icons/left.svg +3 -0
  28. package/_example/client/imgs/icons/line-options.svg +5 -0
  29. package/_example/client/imgs/icons/line.svg +3 -0
  30. package/_example/client/imgs/icons/person.svg +7 -0
  31. package/_example/client/imgs/icons/plus-circle.svg +5 -0
  32. package/_example/client/imgs/icons/plus.svg +5 -0
  33. package/_example/client/imgs/icons/right-circle.svg +3 -0
  34. package/_example/client/imgs/icons/right.svg +3 -0
  35. package/_example/client/imgs/icons/search.svg +3 -0
  36. package/_example/client/imgs/icons/shield.svg +6 -0
  37. package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
  38. package/_example/client/imgs/icons/tick-circle.svg +6 -0
  39. package/_example/client/imgs/icons/tick.svg +5 -0
  40. package/_example/client/imgs/icons/up2-small.svg +4 -0
  41. package/_example/client/imgs/icons/up2.svg +4 -0
  42. package/_example/client/imgs/icons/updown.svg +6 -0
  43. package/_example/client/imgs/icons/v-big-dark.svg +3 -0
  44. package/_example/client/imgs/icons/v-dark.svg +3 -0
  45. package/_example/client/imgs/icons/v.svg +3 -0
  46. package/_example/client/imgs/icons/v2-active.svg +6 -0
  47. package/_example/client/imgs/icons/x1.svg +4 -0
  48. package/_example/client/imgs/logo/logo-white.svg +20 -0
  49. package/_example/client/imgs/logo/logo.svg +20 -0
  50. package/_example/client/imgs/no-image.jpg +0 -0
  51. package/_example/client/imgs/user.jpg +0 -0
  52. package/_example/client/index.html +12 -0
  53. package/_example/client/index.ts +47 -0
  54. package/_example/components/auth.api.js +1 -0
  55. package/_example/components/index.tsx +225 -0
  56. package/_example/components/partials/layouts.tsx +5 -0
  57. package/_example/components/settings.api.js +1 -0
  58. package/_example/server/config.js +120 -0
  59. package/_example/server/email/welcome.html +27 -0
  60. package/_example/server/index.js +32 -0
  61. package/_example/tailwind.config.js +84 -0
  62. package/_example/tsconfig.json +32 -0
  63. package/_example/types.d.ts +7 -0
  64. package/_example/webpack.config.js +4 -0
  65. package/client/app.js +300 -0
  66. package/client/css/components.css +84 -0
  67. package/client/css/fonts.css +67 -0
  68. package/client/imgs/icons/calendar.svg +3 -0
  69. package/client/imgs/icons/email.svg +6 -0
  70. package/client/imgs/icons/eye-open.svg +4 -0
  71. package/client/imgs/icons/eye.svg +5 -0
  72. package/client/imgs/icons/filter.svg +7 -0
  73. package/client/imgs/icons/left-circle.svg +3 -0
  74. package/client/imgs/icons/left.svg +3 -0
  75. package/client/imgs/icons/line-options.svg +5 -0
  76. package/client/imgs/icons/line.svg +3 -0
  77. package/client/imgs/icons/person.svg +7 -0
  78. package/client/imgs/icons/plus-circle.svg +5 -0
  79. package/client/imgs/icons/plus.svg +5 -0
  80. package/client/imgs/icons/right-circle.svg +3 -0
  81. package/client/imgs/icons/right.svg +3 -0
  82. package/client/imgs/icons/search.svg +3 -0
  83. package/client/imgs/icons/shield.svg +6 -0
  84. package/client/imgs/icons/tick-circle-solid.svg +8 -0
  85. package/client/imgs/icons/tick-circle.svg +6 -0
  86. package/client/imgs/icons/tick.svg +5 -0
  87. package/client/imgs/icons/up2-small.svg +4 -0
  88. package/client/imgs/icons/up2.svg +4 -0
  89. package/client/imgs/icons/updown.svg +6 -0
  90. package/client/imgs/icons/v-big-dark.svg +3 -0
  91. package/client/imgs/icons/v-dark.svg +3 -0
  92. package/client/imgs/icons/v.svg +3 -0
  93. package/client/imgs/icons/v2-active.svg +6 -0
  94. package/client/imgs/icons/x1.svg +4 -0
  95. package/client.js +42 -0
  96. package/components/auth/auth.api.js +419 -0
  97. package/components/auth/reset.jsx +88 -0
  98. package/components/auth/signin.jsx +74 -0
  99. package/components/auth/signup.jsx +62 -0
  100. package/components/billing/stripe.api.js +267 -0
  101. package/components/partials/element/accordion.jsx +82 -0
  102. package/components/partials/element/avatar.jsx +28 -0
  103. package/components/partials/element/button.jsx +66 -0
  104. package/components/partials/element/dropdown.jsx +185 -0
  105. package/components/partials/element/initials.jsx +56 -0
  106. package/components/partials/element/message.jsx +124 -0
  107. package/components/partials/element/modal.jsx +229 -0
  108. package/components/partials/element/sidebar.jsx +166 -0
  109. package/components/partials/element/tooltip.jsx +146 -0
  110. package/components/partials/element/topbar.jsx +25 -0
  111. package/components/partials/form/checkbox.jsx +74 -0
  112. package/components/partials/form/drop-handler.jsx +62 -0
  113. package/components/partials/form/drop.jsx +125 -0
  114. package/components/partials/form/form-error.jsx +21 -0
  115. package/components/partials/form/input-color.jsx +77 -0
  116. package/components/partials/form/input-currency.jsx +133 -0
  117. package/components/partials/form/input-date.jsx +223 -0
  118. package/components/partials/form/input.jsx +131 -0
  119. package/components/partials/form/location.jsx +212 -0
  120. package/components/partials/form/select.jsx +369 -0
  121. package/components/partials/form/toggle.jsx +46 -0
  122. package/components/partials/is-first-render.js +15 -0
  123. package/components/partials/layout/layout1.jsx +32 -0
  124. package/components/partials/layout/layout2.jsx +47 -0
  125. package/components/partials/not-found.jsx +7 -0
  126. package/components/partials/styleguide.jsx +252 -0
  127. package/components/settings/settings-account.jsx +143 -0
  128. package/components/settings/settings-business.jsx +121 -0
  129. package/components/settings/settings-team--member.jsx +108 -0
  130. package/components/settings/settings-team.jsx +76 -0
  131. package/components/settings/settings.api.js +54 -0
  132. package/package.json +175 -0
  133. package/readme.md +43 -0
  134. package/server/email/index.js +192 -0
  135. package/server/email/partials/email.css +153 -0
  136. package/server/email/partials/layout1.swig +92 -0
  137. package/server/email/partials/line.swig +8 -0
  138. package/server/email/partials/vert-10.swig +8 -0
  139. package/server/email/partials/vert-15.swig +8 -0
  140. package/server/email/partials/vert-20.swig +8 -0
  141. package/server/email/partials/vert-25.swig +8 -0
  142. package/server/email/partials/vert-30.swig +8 -0
  143. package/server/email/partials/vert-35.swig +8 -0
  144. package/server/email/partials/vert-50.swig +8 -0
  145. package/server/email/reset-password.html +21 -0
  146. package/server/email/welcome.html +21 -0
  147. package/server/models/company.js +76 -0
  148. package/server/models/user.js +45 -0
  149. package/server/router.js +355 -0
  150. package/server.js +20 -0
  151. package/util.js +1145 -0
  152. package/webpack.config.js +302 -0
package/client.js ADDED
@@ -0,0 +1,42 @@
1
+ export * from './client/app.js'
2
+ export * from './util.js'
3
+ export * as util from './util.js'
4
+
5
+ // Components: Pages
6
+ export { Signin } from './components/auth/signin.jsx'
7
+ export { Signup } from './components/auth/signup.jsx'
8
+ export { ResetInstructions, ResetPassword } from './components/auth/reset.jsx'
9
+ // export { SettingsAccount } from './components/settings/settings-account.jsx'
10
+ // export { SettingsBusiness } from './components/settings/settings-business.jsx'
11
+ // export { SettingsTeamMember } from './components/settings/settings-team--member.jsx'
12
+ // export { SettingsTeam } from './components/settings/settings-team.jsx'
13
+
14
+ // Components: partials: elements
15
+ export { Accordion } from './components/partials/element/accordion.jsx'
16
+ export { Avatar } from './components/partials/element/avatar.jsx'
17
+ export { Button } from './components/partials/element/button.jsx'
18
+ export { Dropdown } from './components/partials/element/dropdown.jsx'
19
+ export { Initials } from './components/partials/element/initials.jsx'
20
+ export { Message } from './components/partials/element/message.jsx'
21
+ // export { Modal } from './components/partials/element/modal.jsx'
22
+ export { Sidebar } from './components/partials/element/sidebar.jsx'
23
+ export { Tooltip } from './components/partials/element/tooltip.jsx'
24
+ export { Topbar } from './components/partials/element/topbar.jsx'
25
+
26
+ // Components: partials: form
27
+ export { Checkbox } from './components/partials/form/checkbox.jsx'
28
+ export { Drop } from './components/partials/form/drop.jsx'
29
+ export { FormError } from './components/partials/form/form-error.jsx'
30
+ export { Input } from './components/partials/form/input.jsx'
31
+ export { Location } from './components/partials/form/location.jsx'
32
+ export { Select } from './components/partials/form/select.jsx'
33
+ export { Toggle } from './components/partials/form/toggle.jsx'
34
+
35
+ // Components: partials: layouts
36
+ export { Layout1 } from './components/partials/layout/layout1.jsx'
37
+ export { Layout2 } from './components/partials/layout/layout2.jsx'
38
+
39
+ // Components: partials: other
40
+ export { IsFirstRender } from './components/partials/is-first-render.js'
41
+ export { NotFound } from './components/partials/not-found.jsx'
42
+ export { Styleguide } from './components/partials/styleguide.jsx'
@@ -0,0 +1,419 @@
1
+ import MongoStore from 'connect-mongo'
2
+ import crypto from 'crypto'
3
+ import expressSession from 'express-session'
4
+ import passport from 'passport'
5
+ import passportLocal from 'passport-local'
6
+ import db from 'monastery'
7
+ import { sendEmail } from '../../server/email/index.js'
8
+ import * as util from '../../util.js'
9
+ // import stripeController from '../billing/stripe.api.js'
10
+
11
+ let config = {}
12
+
13
+ export default {
14
+
15
+ routes: {
16
+ 'get /api/state': ['state'],
17
+ 'get /api/signout': ['signout'],
18
+ 'post /api/signin': ['signin'],
19
+ 'post /api/signup': ['signup'],
20
+ 'post /api/reset-instructions': ['resetInstructions'],
21
+ 'post /api/reset-password': ['resetPassword'],
22
+ 'delete /api/account/:uid': ['isUser', 'remove'],
23
+ },
24
+
25
+ setup: function (middleware, _config) {
26
+ // Setup passport handlers for reading and writing to req.session
27
+ const that = this
28
+ global.passport = passport
29
+
30
+ // Set config values
31
+ config = {
32
+ env: _config.env,
33
+ masterPassword: _config.masterPassword,
34
+ }
35
+ for (let key in config) {
36
+ if (!config[key] && key != 'masterPassword') {
37
+ throw new Error(`Missing config value for stripe.api.js: ${key}`)
38
+ }
39
+ }
40
+
41
+ // After successful login, serialize the user into a session object
42
+ passport.serializeUser((user, next) => {
43
+ next(null, { _id: user._id })
44
+ })
45
+
46
+ // After session read, get the user from the session object
47
+ passport.deserializeUser(async (sessionObject, next) => {
48
+ try {
49
+ const user = await that._findUserFromProvider('deserialize', sessionObject)
50
+ next(null, user)
51
+ } catch (err) {
52
+ next(err.message)
53
+ }
54
+ })
55
+
56
+ // Setup passport local signin strategy
57
+ passport.use(
58
+ new passportLocal.Strategy(
59
+ { usernameField: 'email' },
60
+ async (email, password, next) => {
61
+ try {
62
+ const user = await that._findUserFromProvider('email', { email: email, password: password })
63
+ next(null, user)
64
+ } catch (err) {
65
+ next(err.message)
66
+ }
67
+ }
68
+ )
69
+ )
70
+
71
+ // https://medium.com/swlh/everything-you-need-to-know-about-the-passport-jwt-passport-js-strategy-8b69f39014b0
72
+ // https://github.com/mikenicholson/passport-jwt
73
+ //
74
+ // passport.use(new JwtStrategy.Strategy({
75
+ // jwtFromRequest: JwtStrategy.ExtractJwt.fromAuthHeaderAsBearerToken(),
76
+ // secretOrKey: '1fjw3h3jkdJD8sjA12dw53llapA2sjAjsv3nxaxzNBzz',
77
+ // }, function(jwtPayload, done) {
78
+ //
79
+ // this._findUserFromProvider('email', { email: email, password: password }, done)
80
+ // console.log(jwtPayload)
81
+ // User.findOne({id: jwt_payload.sub}, function(err, user) {
82
+ // if (err) {
83
+ // return done(err, false);
84
+ // }
85
+ // if (user) {
86
+ // return done(null, user);
87
+ // } else {
88
+ // return done(null, false);
89
+ // // or you could create a new account
90
+ // }
91
+ // });
92
+ // }));
93
+
94
+ // Add session middleware
95
+ middleware.order.splice(3, 0, 'session', 'passport', 'passportSession', 'passportError', 'blocked')
96
+ Object.assign(middleware, {
97
+ blocked: function (req, res, next) {
98
+ if (req.user && req.user.loginActive === false) {
99
+ req.logout()
100
+ res.error('This user is not available.')
101
+ } else {
102
+ next()
103
+ }
104
+ },
105
+ passport: passport.initialize(),
106
+ passportError: function (err, req, res, next) {
107
+ if (!err) return next()
108
+ req.logout()
109
+ res.error(err)
110
+ },
111
+ passportSession: passport.session(),
112
+ session: expressSession({
113
+ secret: '092720e5ffc1237266b8517239cd81b6', // Changing invalidates cookies
114
+ cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 },
115
+ resave: false,
116
+ saveUninitialized: false,
117
+ store: MongoStore.create({
118
+ clientPromise: db.onOpen((manager) => {
119
+ return manager.client
120
+ }),
121
+ }),
122
+ }),
123
+ })
124
+ },
125
+
126
+ state: async function (req, res) {
127
+ res.json(await this._getState(req.user))
128
+ },
129
+
130
+ signup: async function (req, res) {
131
+ try {
132
+ let user = await this._userCreate(req.body)
133
+ // Welcome email
134
+ sendEmail({
135
+ config: config,
136
+ template: 'welcome',
137
+ to: `${util.ucFirst(user.firstName)}<${user.email}>`,
138
+ }).catch(err => {
139
+ console.error(err)
140
+ })
141
+ // Login
142
+ res.send(await this._signinAndGetState(req, user))
143
+ } catch (err) {
144
+ res.error(err)
145
+ }
146
+ },
147
+
148
+ signin: function (req, res) {
149
+ // console.log('api: signin')
150
+ // console.log(req.body)
151
+ if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
152
+ if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
153
+ passport.authenticate('local', { session: false }, async (err, user, info) => {
154
+ if (err) return console.log(err) || res.error(err)
155
+ if (!user && info) return res.error('email', info.message)
156
+ try {
157
+ const response = await this._signinAndGetState(req, user)
158
+ res.send(response)
159
+ } catch (err) {
160
+ res.error(err)
161
+ }
162
+ })(req, res)
163
+ },
164
+
165
+ signout: function (req, res) {
166
+ req.logout()
167
+ res.json('{}')
168
+ },
169
+
170
+ resetInstructions: async function (req, res) {
171
+ try {
172
+ let email = (req.body.email||'').trim().toLowerCase()
173
+ if (!email || !util.isString(email)) {
174
+ throw { title: 'email', detail: 'The email you entered is incorrect.' }
175
+ }
176
+ // Find matching user and create new reset token
177
+ let user = await db.user.findOne({ query: { email: email }, _privateData: true })
178
+ if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
179
+ // Create token
180
+ let token = await this._tokenCreate(user._id)
181
+ // Update user with token
182
+ await db.user.update({ query: { email: email }, $set: { resetToken: token }})
183
+ // Email.
184
+ res.json({})
185
+ sendEmail({
186
+ config: config,
187
+ template: 'reset-password',
188
+ to: `${util.ucFirst(user.firstName)}<${email}>`,
189
+ data: {
190
+ token: token + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
191
+ },
192
+ }).catch(err => {
193
+ console.error('sendEmail(..) mailgun error', err)
194
+ })
195
+ } catch (err) {
196
+ res.error(err)
197
+ }
198
+ },
199
+
200
+ resetPassword: async function (req, res) {
201
+ try {
202
+ const { token, password, password2 } = req.body
203
+ const id = this._tokenParse(token)
204
+ // Validate password
205
+ this._validatePassword(password, password2)
206
+ // Find matching user
207
+ let user = await db.user.findOne({
208
+ query: id,
209
+ blacklist: ['-resetToken'],
210
+ _privateData: true,
211
+ })
212
+ if (!user || user.resetToken !== token) {
213
+ throw new Error('Sorry your email token is invalid or has already been used verify your email.')
214
+ }
215
+ // Update user with new password
216
+ await db.user.update({
217
+ query: user._id,
218
+ data: {
219
+ password: await (await import('bcrypt')).hash(password, 10),
220
+ resetToken: '',
221
+ },
222
+ blacklist: ['-resetToken', '-password'],
223
+ })
224
+ res.send(await this._signinAndGetState(req, { ...user, resetToken: undefined }))
225
+ } catch (err) {
226
+ res.error(err)
227
+ }
228
+ },
229
+
230
+ remove: async function (req, res) {
231
+ try {
232
+ const uid = db.id(req.params.uid || 'badid')
233
+
234
+ // Get companies owned by user
235
+ const companyIdsOwned = (await db.company.find({
236
+ query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
237
+ project: { _id: 1 },
238
+ })).map(o => o._id)
239
+
240
+ // Check for active subscription first...
241
+ if (req.user.stripeSubscription?.status == 'active') {
242
+ throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
243
+ }
244
+
245
+ if (companyIdsOwned.length) {
246
+ await db.transaction.remove({ query: { company: { $in: companyIdsOwned }}})
247
+ await db.statement.remove({ query: { company: { $in: companyIdsOwned }}})
248
+ await db.account.remove({ query: { company: { $in: companyIdsOwned }}})
249
+ await db.document.remove({ query: { company: { $in: companyIdsOwned }}})
250
+ await db.contact.remove({ query: { company: { $in: companyIdsOwned }}})
251
+ await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
252
+ await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
253
+ }
254
+ await db.user.remove({ query: { _id: uid }})
255
+ // Logout now so that an error doesn't throw when naviating to /signout
256
+ req.logout()
257
+ res.send(`User: '${uid}' and companies: '${companyIdsOwned.join(', ')}' removed successfully`)
258
+ } catch (err) {
259
+ res.error(err)
260
+ }
261
+ },
262
+
263
+ /* ---- Private fns ---------------- */
264
+
265
+ _getState: async function (user) {
266
+ // Format the initial state
267
+ return {
268
+ user: user || null,
269
+ // stripeProducts: await stripeController._getProducts(),
270
+ }
271
+ },
272
+
273
+ _signinAndGetState: function (req, user) {
274
+ // @return state
275
+ return new Promise((resolve, reject) => {
276
+ user.desktop = req.query.hasOwnProperty('desktop')
277
+ if (user.loginActive !== false) {
278
+ req.login(user, async (err) => {
279
+ if (err) return reject(err)
280
+ resolve(await this._getState(user))
281
+ })
282
+ } else {
283
+ return reject('This user is not available.')
284
+ // this._getState().then((state) => resolve(state))
285
+ }
286
+ })
287
+ },
288
+
289
+ _tokenCreate: function (id) {
290
+ return new Promise((resolve) => {
291
+ crypto.randomBytes(16, (err, buff) => {
292
+ let hash = buff.toString('hex') // 32 chars
293
+ resolve(`${hash}${id || ''}:${Date.now()}`)
294
+ })
295
+ })
296
+ },
297
+
298
+ _tokenParse: function (token) {
299
+ let split = (token || '').split(':')
300
+ let hash = split[0].slice(0, 32)
301
+ let userId = split[0].slice(32)
302
+ let time = split[1]
303
+ if (!hash || !userId || !time) {
304
+ throw { title: 'error', detail: 'Sorry your code is invalid.' }
305
+ } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
306
+ throw { title: 'error', detail: 'Sorry your code has timed out.' }
307
+ } else {
308
+ return userId
309
+ }
310
+ },
311
+
312
+ _validatePassword: async function (password='', password2) {
313
+ // let hasLowerChar = password.match(/[a-z]/)
314
+ // let hasUpperChar = password.match(/[A-Z]/)
315
+ // let hasNumber = password.match(/\d/)
316
+ // let hasSymbol = password.match(/\W/)
317
+ if (!password) {
318
+ throw [{ title: 'password', detail: 'This field is required.' }]
319
+ } else if (config.env !== 'development' && password.length < 8) {
320
+ throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
321
+ // } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
322
+ // throw {
323
+ // title: 'password',
324
+ // detail: 'You need to include uppercase and lowercase letters, and a number'
325
+ // }
326
+ } else if (typeof password2 != 'undefined' && password !== password2) {
327
+ throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
328
+ }
329
+ },
330
+
331
+ _userCreate: async function ({ name, business, email, password }) {
332
+ try {
333
+ const options = { skipValidation: ['business.address', 'tax'], blacklist: ['-_id'] }
334
+ const userId = db.id()
335
+ const companyData = {
336
+ _id: db.id(),
337
+ business: business,
338
+ email: email,
339
+ users: [{ _id: userId, role: 'owner', status: 'active' }],
340
+ }
341
+ const userData = {
342
+ _id: userId,
343
+ company: companyData._id,
344
+ email: email,
345
+ firstName: util.fullNameSplit(name)[0],
346
+ lastName: util.fullNameSplit(name)[1],
347
+ password: await (await import('bcrypt')).hash(password || 'temp', 10),
348
+ }
349
+
350
+ // First validate the data so we don't have to create a transaction
351
+ const results = await Promise.allSettled([
352
+ db.user.validate(userData, options),
353
+ db.company.validate(companyData, options),
354
+ this._validatePassword(password),
355
+ ])
356
+
357
+ // Throw all the errors from at once
358
+ const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
359
+ if (util.isArray(o.reason)) acc.push(...o.reason)
360
+ else throw o.reason
361
+ return acc
362
+ }, [])
363
+ if (errors.length) throw errors
364
+
365
+ // Insert company & user
366
+ await db.user.insert({ data: userData, ...options })
367
+ await db.company.insert({ data: companyData, ...options })
368
+
369
+ // Return the user
370
+ return await this._findUserFromProvider('deserialize', { _id: userId })
371
+
372
+ } catch (err) {
373
+ if (!util.isArray(err)) throw err
374
+ throw err.map((o) => {
375
+ if (o.title == 'firstName') o.title = 'name'
376
+ return o
377
+ })
378
+ }
379
+ },
380
+
381
+ _findUserFromProvider: async function (type, { _id, email, password }) {
382
+ /**
383
+ * Find user for state (and verify password if signing in with email)
384
+ * @param {string} type - 'deserialize' or 'email' (jwt, google, etc)
385
+ * @param {string} data - req.data
386
+ */
387
+ const user = await db.user.findOne({
388
+ query: type == 'email' ? { email } : _id,
389
+ blacklist: ['-password'],
390
+ populate: db.user.loginPopulate(),
391
+ _privateData: true,
392
+ })
393
+ if (user?.company) {
394
+ user.company = await db.company.findOne({
395
+ query: user.company,
396
+ populate: db.company.loginPopulate(),
397
+ _privateData: true,
398
+ })
399
+ }
400
+ if (!user) {
401
+ throw new Error(type == 'email' ? 'Email or password is incorrect.' : 'Session user is invalid.')
402
+ } else if (!user.company) {
403
+ throw new Error('The current company is no longer associated with this user')
404
+ } else if (user.company.status != 'active') {
405
+ throw new Error('This user is not associated with an active company')
406
+ } else {
407
+ if (type == 'email') {
408
+ const match = await (await import('bcrypt')).compare(password, user.password || 'no-pass')
409
+ if (!match && !(config.masterPassword && password == config.masterPassword)) {
410
+ throw new Error('Email or password is incorrect.')
411
+ }
412
+ }
413
+ // Successful return user
414
+ delete user.password
415
+ return user
416
+ }
417
+ },
418
+
419
+ }
@@ -0,0 +1,88 @@
1
+ import * as util from '../../util.js'
2
+ import { Topbar } from '../partials/element/topbar.jsx'
3
+ import { Input } from '../partials/form/input.jsx'
4
+ import { FormError } from '../partials/form/form-error.jsx'
5
+ import { Button } from '../partials/element/button.jsx'
6
+
7
+ export function ResetInstructions() {
8
+ const navigate = useNavigate()
9
+ const isLoading = useState('')
10
+ const [, setStore] = sharedStore.useTracked()
11
+ const [state, setState] = useState({ email: '' })
12
+
13
+ async function onSubmit (e) {
14
+ try {
15
+ await util.request(e, 'post /api/reset-instructions', state, isLoading)
16
+ setStore(s => ({ ...s, message: 'Done! Please check your email.' }))
17
+ navigate('/signin')
18
+ } catch (errors) {
19
+ return setState({ ...state, errors })
20
+ }
21
+ }
22
+
23
+ return (
24
+ <div class="">
25
+ <Topbar title={<>Reset your Password</>} />
26
+
27
+ <form onSubmit={onSubmit}>
28
+ <div>
29
+ <label for="email">Email Address</label>
30
+ <Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
31
+ </div>
32
+
33
+ <div class="mb-14">
34
+ Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
35
+ <FormError state={state} class="pt-2" />
36
+ </div>
37
+
38
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Email me a reset password link</Button>
39
+ </form>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export function ResetPassword() {
45
+ const navigate = useNavigate()
46
+ const params = useParams()
47
+ const isLoading = useState('')
48
+ const [, setStore] = sharedStore.useTracked()
49
+ const [state, setState] = useState(() => ({
50
+ password: '',
51
+ password2: '',
52
+ token: params.token,
53
+ }))
54
+
55
+ async function onSubmit (e) {
56
+ try {
57
+ const data = await util.request(e, 'post /api/reset-password', state, isLoading)
58
+ setStore(() => data)
59
+ navigate('/')
60
+ } catch (errors) {
61
+ return setState({ ...state, errors })
62
+ }
63
+ }
64
+
65
+ return (
66
+ <div class="">
67
+ <Topbar title={<>Reset your Password</>} />
68
+
69
+ <form onSubmit={onSubmit}>
70
+ <div>
71
+ <label for="password">Your New Password</label>
72
+ <Input name="password" type="password" state={state} onChange={onChange(setState)} />
73
+ </div>
74
+ <div>
75
+ <label for="password2">Repeat Your New Password</label>
76
+ <Input name="password2" type="password" state={state} onChange={onChange(setState)} />
77
+ </div>
78
+
79
+ <div class="mb-14">
80
+ Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
81
+ <FormError state={state} class="pt-2" />
82
+ </div>
83
+
84
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
85
+ </form>
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,74 @@
1
+ import * as util from '../../util.js'
2
+ import { Topbar } from '../partials/element/topbar.jsx'
3
+ import { Input } from '../partials/form/input.jsx'
4
+ import { Button } from '../partials/element/button.jsx'
5
+ import { FormError } from '../partials/form/form-error.jsx'
6
+
7
+ export function Signin({ config }) {
8
+ const navigate = useNavigate()
9
+ const location = useLocation()
10
+ const isSignout = location.pathname == '/signout'
11
+ const isLoading = useState(isSignout ? 'is-loading' : '')
12
+ const [, setStore] = sharedStore.useTracked()
13
+ const [state, setState] = useState({
14
+ email: config.env == 'development' ? config.testEmail : '',
15
+ password: config.env == 'development' ? '1234' : '',
16
+ })
17
+
18
+ useEffect(() => {
19
+ // Autofill the email input from ?email=
20
+ const query = util.queryObject(location.search, true)
21
+ if (query.email) setState({ ...state, email: query.email })
22
+ }, [location.search])
23
+
24
+ useEffect(() => {
25
+ if (isSignout) {
26
+ setStore(() => ({ user: null }))
27
+ util.axios().get('/api/signout')
28
+ .then(() => isLoading[1](''))
29
+ .then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
30
+ .catch(err => console.error(err))
31
+ }
32
+ }, [isSignout])
33
+
34
+ async function onSubmit (e) {
35
+ try {
36
+ const data = await util.request(e, 'post /api/signin', state, isLoading)
37
+ isLoading[1]('is-loading')
38
+ setStore(() => data)
39
+ setTimeout(() => { // wait for setStore
40
+ if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
41
+ else navigate('/')
42
+ }, 100)
43
+ } catch (errors) {
44
+ return setState({ ...state, errors })
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div>
50
+ <Topbar title={<>Sign in to your Account</>} />
51
+
52
+ <form onSubmit={onSubmit}>
53
+ <div>
54
+ <label for="email">Email Address</label>
55
+ <Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
56
+ </div>
57
+ <div>
58
+ <div class="flex justify-between">
59
+ <label for="password">Password</label>
60
+ <Link to="/reset" class="label underline2">Forgot?</Link>
61
+ </div>
62
+ <Input name="password" type="password" state={state} onChange={onChange(setState)}/>
63
+ </div>
64
+
65
+ <div class="mb-14">
66
+ Don&apos;t have an account? You can <Link to="/signup" class="underline2 is-active">sign up here</Link>.
67
+ <FormError state={state} class="pt-2" />
68
+ </div>
69
+
70
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
71
+ </form>
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,62 @@
1
+ import * as util from '../../util.js'
2
+ import { Topbar } from '../partials/element/topbar.jsx'
3
+ import { Input } from '../partials/form/input.jsx'
4
+ import { Button } from '../partials/element/button.jsx'
5
+ import { FormError } from '../partials/form/form-error.jsx'
6
+
7
+ export function Signup({ config }) {
8
+ const navigate = useNavigate()
9
+ const isLoading = useState('')
10
+ const [, setStore] = sharedStore.useTracked()
11
+ const [state, setState] = useState({
12
+ email: config.env === 'development' ? config.testEmail : '',
13
+ name: config.env === 'development' ? 'Bruce Wayne' : '',
14
+ business: { name: config.env === 'development' ? 'Wayne Enterprises' : '' },
15
+ password: config.env === 'development' ? '1234' : '',
16
+ })
17
+
18
+ async function onSubmit (e) {
19
+ try {
20
+ const data = await util.request(e, 'post /api/signup', state, isLoading)
21
+ isLoading[1]('is-loading')
22
+ setStore(() => data)
23
+ setTimeout(() => navigate('/'), 0) // wait for setStore
24
+ } catch (errors) {
25
+ return setState({ ...state, errors })
26
+ }
27
+ }
28
+
29
+ return (
30
+ <div class="">
31
+ <Topbar title={<>Start your 21 day Free Trial</>} />
32
+
33
+ <form onSubmit={onSubmit}>
34
+ <div class="grid grid-cols-2 gap-6">
35
+ <div>
36
+ <label for="name">Your Name</label>
37
+ <Input name="name" placeholder="E.g. Tony Stark" state={state} onChange={onChange(setState)} />
38
+ </div>
39
+ <div>
40
+ <label for="business.name">Company Name</label>
41
+ <Input name="business.name" placeholder="E.g. Stark Industries" state={state} onChange={onChange(setState)} />
42
+ </div>
43
+ </div>
44
+ <div>
45
+ <label for="email">Email Address</label>
46
+ <Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
47
+ </div>
48
+ <div>
49
+ <label for="password">Password</label>
50
+ <Input name="password" type="password" state={state} onChange={onChange(setState)}/>
51
+ </div>
52
+
53
+ <div class="mb-14">
54
+ Already have an account? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
55
+ <FormError state={state} class="pt-2" />
56
+ </div>
57
+
58
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Create Account</Button>
59
+ </form>
60
+ </div>
61
+ )
62
+ }