nitro-web 0.0.86 → 0.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/globals.ts +10 -6
- package/package.json +14 -6
- package/types/{required-globals.d.ts → globals.d.ts} +3 -1
- package/.editorconfig +0 -9
- package/components/auth/auth.api.js +0 -411
- package/components/auth/reset.tsx +0 -86
- package/components/auth/signin.tsx +0 -76
- package/components/auth/signup.tsx +0 -62
- package/components/billing/stripe.api.js +0 -268
- package/components/dashboard/dashboard.tsx +0 -32
- package/components/partials/element/accordion.tsx +0 -102
- package/components/partials/element/avatar.tsx +0 -40
- package/components/partials/element/button.tsx +0 -98
- package/components/partials/element/calendar.tsx +0 -125
- package/components/partials/element/dropdown.tsx +0 -248
- package/components/partials/element/filters.tsx +0 -194
- package/components/partials/element/github-link.tsx +0 -16
- package/components/partials/element/initials.tsx +0 -66
- package/components/partials/element/message.tsx +0 -141
- package/components/partials/element/modal.tsx +0 -90
- package/components/partials/element/sidebar.tsx +0 -195
- package/components/partials/element/tooltip.tsx +0 -154
- package/components/partials/element/topbar.tsx +0 -15
- package/components/partials/form/checkbox.tsx +0 -150
- package/components/partials/form/drop-handler.tsx +0 -68
- package/components/partials/form/drop.tsx +0 -141
- package/components/partials/form/field-color.tsx +0 -86
- package/components/partials/form/field-currency.tsx +0 -158
- package/components/partials/form/field-date.tsx +0 -252
- package/components/partials/form/field.tsx +0 -231
- package/components/partials/form/form-error.tsx +0 -27
- package/components/partials/form/location.tsx +0 -225
- package/components/partials/form/select.tsx +0 -360
- package/components/partials/is-first-render.ts +0 -14
- package/components/partials/not-found.tsx +0 -7
- package/components/partials/styleguide.tsx +0 -407
- package/semver-updater.cjs +0 -13
- package/tsconfig.json +0 -38
- package/tsconfig.types.json +0 -15
- package/types/core-only-globals.d.ts +0 -9
- package/types.ts +0 -60
package/client/globals.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, Dispatch, SetStateAction } from 'react'
|
|
2
2
|
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
3
3
|
import { onChange } from 'nitro-web'
|
|
4
|
+
import { Store } from 'nitro-web/types'
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
6
|
-
//
|
|
7
|
-
const
|
|
7
|
+
// useTracked global (normally defined in ./client/index.ts)
|
|
8
|
+
const useTracked: () => [Store, Dispatch<SetStateAction<Store>>]
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// nitro-web global
|
|
11
|
+
const onChange: typeof import('nitro-web').onChange
|
|
12
|
+
|
|
13
|
+
// common daependency globals
|
|
10
14
|
/** The public API for rendering a history-aware `<a>`. */
|
|
11
15
|
const Link: typeof import('react-router-dom').Link
|
|
12
16
|
const useCallback: typeof import('react').useCallback
|
|
@@ -21,9 +25,9 @@ declare global {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
Object.assign(window, {
|
|
24
|
-
//
|
|
28
|
+
// nitro-web global
|
|
25
29
|
onChange: onChange,
|
|
26
|
-
// dependency globals
|
|
30
|
+
// common dependency globals
|
|
27
31
|
Link: Link,
|
|
28
32
|
useCallback: useCallback,
|
|
29
33
|
useEffect: useEffect,
|
package/package.json
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.87",
|
|
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 🚀",
|
|
7
|
-
"main": "./client/index.ts",
|
|
8
7
|
"type": "module",
|
|
8
|
+
"main": "./client/index.ts",
|
|
9
|
+
"types": "./types/globals.d.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": "./client/index.ts",
|
|
12
|
+
"./.eslintrc.json": "./.eslintrc.json",
|
|
11
13
|
"./client/imgs/*": "./client/imgs/*",
|
|
12
14
|
"./client/globals": "./client/globals.ts",
|
|
13
15
|
"./server": "./server/index.js",
|
|
14
16
|
"./types": "./types.ts",
|
|
15
|
-
"./.eslintrc.json": "./.eslintrc.json",
|
|
16
|
-
"./tsconfig.json": "./tsconfig.json",
|
|
17
|
-
"./webpack.config.js": "./webpack.config.js",
|
|
18
17
|
"./util": {
|
|
19
18
|
"require": "./util.js",
|
|
20
19
|
"import": "./util.js",
|
|
21
20
|
"types": "./types/util.d.ts"
|
|
22
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"./webpack.config.js": "./webpack.config.js"
|
|
23
23
|
},
|
|
24
|
+
"files": [
|
|
25
|
+
".eslintrc.json",
|
|
26
|
+
"client",
|
|
27
|
+
"server",
|
|
28
|
+
"types",
|
|
29
|
+
"util.js",
|
|
30
|
+
"webpack.config.js"
|
|
31
|
+
],
|
|
24
32
|
"scripts": {
|
|
25
33
|
"major": "npm run types && standard-version -a --release-as major && npm publish && cd ../webpack && npm publish",
|
|
26
34
|
"minor": "npm run types && standard-version -a --release-as minor && npm publish && cd ../webpack && npm publish",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Required global types
|
|
2
1
|
import 'twin.macro'
|
|
3
2
|
import { css as cssImport } from '@emotion/react'
|
|
4
3
|
import styledImport from '@emotion/styled'
|
|
@@ -17,6 +16,7 @@ declare global {
|
|
|
17
16
|
const content: string
|
|
18
17
|
export default content
|
|
19
18
|
}
|
|
19
|
+
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Webpack: Twin.macro css extension
|
|
@@ -37,3 +37,5 @@ declare module 'react' {
|
|
|
37
37
|
for?: string | undefined
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
export {}
|
package/.editorconfig
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import crypto from 'crypto'
|
|
3
|
-
import bcrypt from 'bcrypt'
|
|
4
|
-
import passport from 'passport'
|
|
5
|
-
import passportLocal from 'passport-local'
|
|
6
|
-
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
|
|
7
|
-
import db from 'monastery'
|
|
8
|
-
import jsonwebtoken from 'jsonwebtoken'
|
|
9
|
-
import { sendEmail } from 'nitro-web/server'
|
|
10
|
-
import { isArray, pick, ucFirst, fullNameSplit } from 'nitro-web/util'
|
|
11
|
-
|
|
12
|
-
let authConfig = null
|
|
13
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
|
|
14
|
-
|
|
15
|
-
export const routes = {
|
|
16
|
-
// Routes
|
|
17
|
-
'get /api/store': [store],
|
|
18
|
-
'get /api/signout': [signout],
|
|
19
|
-
'post /api/signin': [signin],
|
|
20
|
-
'post /api/signup': [signup],
|
|
21
|
-
'post /api/reset-instructions': [resetInstructions],
|
|
22
|
-
'post /api/reset-password': [resetPassword],
|
|
23
|
-
'post /api/invite-instructions': [inviteInstructions],
|
|
24
|
-
'post /api/invite-accept': [resetPassword],
|
|
25
|
-
'delete /api/account/:uid': [remove],
|
|
26
|
-
|
|
27
|
-
// Overridable helpers
|
|
28
|
-
setup: setup,
|
|
29
|
-
findUserFromProvider: findUserFromProvider,
|
|
30
|
-
getStore: getStore,
|
|
31
|
-
signinAndGetStore: signinAndGetStore,
|
|
32
|
-
tokenCreate: tokenCreate,
|
|
33
|
-
tokenParse: tokenParse,
|
|
34
|
-
userCreate: userCreate,
|
|
35
|
-
validatePassword: validatePassword,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function setup(middleware, _config) {
|
|
39
|
-
// routes.setup is called automatically when express starts
|
|
40
|
-
// Set config values
|
|
41
|
-
const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant']
|
|
42
|
-
authConfig = pick(_config, configKeys)
|
|
43
|
-
for (const key of ['clientUrl', 'emailFrom', 'env', 'name']) {
|
|
44
|
-
if (!authConfig[key]) throw new Error(`Missing config value for: config.${key}`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
passport.use(
|
|
48
|
-
new passportLocal.Strategy(
|
|
49
|
-
{ usernameField: 'email' },
|
|
50
|
-
async (email, password, next) => {
|
|
51
|
-
try {
|
|
52
|
-
const user = await this.findUserFromProvider({ email }, password)
|
|
53
|
-
next(null, user)
|
|
54
|
-
} catch (err) {
|
|
55
|
-
next(err.message)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
)
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
passport.use(
|
|
62
|
-
new JwtStrategy(
|
|
63
|
-
{
|
|
64
|
-
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
65
|
-
secretOrKey: JWT_SECRET,
|
|
66
|
-
},
|
|
67
|
-
async (payload, done) => {
|
|
68
|
-
try {
|
|
69
|
-
const user = await this.findUserFromProvider({ _id: payload._id })
|
|
70
|
-
if (!user) return done(null, false)
|
|
71
|
-
return done(null, user)
|
|
72
|
-
} catch (err) {
|
|
73
|
-
return done(err, false)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
|
|
80
|
-
|
|
81
|
-
Object.assign(middleware, {
|
|
82
|
-
blocked: function (req, res, next) {
|
|
83
|
-
if (req.user && req.user.loginActive === false) {
|
|
84
|
-
res.status(403).error('This user is not available.')
|
|
85
|
-
} else {
|
|
86
|
-
next()
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
jwtAuth: function(req, res, next) {
|
|
90
|
-
passport.authenticate('jwt', { session: false }, function(err, user) {
|
|
91
|
-
if (user) req.user = user
|
|
92
|
-
next()
|
|
93
|
-
})(req, res, next)
|
|
94
|
-
},
|
|
95
|
-
passport: passport.initialize(),
|
|
96
|
-
passportError: function (err, req, res, next) {
|
|
97
|
-
if (!err) return next()
|
|
98
|
-
res.error(err)
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function store(req, res) {
|
|
104
|
-
res.json(await this.getStore(req.user))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function signup(req, res) {
|
|
108
|
-
try {
|
|
109
|
-
const desktop = req.query.desktop
|
|
110
|
-
let user = await this.userCreate(req.body)
|
|
111
|
-
sendEmail({
|
|
112
|
-
config: authConfig,
|
|
113
|
-
template: 'welcome',
|
|
114
|
-
to: `${ucFirst(user.firstName)}<${user.email}>`,
|
|
115
|
-
}).catch(console.error)
|
|
116
|
-
res.send(await this.signinAndGetStore(user, desktop, this.getStore))
|
|
117
|
-
} catch (err) {
|
|
118
|
-
res.error(err)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function signin(req, res) {
|
|
123
|
-
const desktop = req.query.desktop
|
|
124
|
-
if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
|
|
125
|
-
if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
|
|
126
|
-
|
|
127
|
-
passport.authenticate('local', { session: false }, async (err, user, info) => {
|
|
128
|
-
if (err) return res.error(err)
|
|
129
|
-
if (!user && info) return res.error('email', info.message)
|
|
130
|
-
try {
|
|
131
|
-
const response = await this.signinAndGetStore(user, desktop, this.getStore)
|
|
132
|
-
res.send(response)
|
|
133
|
-
} catch (err) {
|
|
134
|
-
res.error(err)
|
|
135
|
-
}
|
|
136
|
-
})(req, res)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function signout(req, res) {
|
|
140
|
-
res.json('{}')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function resetInstructions(req, res) {
|
|
144
|
-
try {
|
|
145
|
-
let email = (req.body.email || '').trim().toLowerCase()
|
|
146
|
-
if (!email) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
147
|
-
|
|
148
|
-
let user = await db.user.findOne({ query: { email }, _privateData: true })
|
|
149
|
-
if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
150
|
-
|
|
151
|
-
let resetToken = await tokenCreate(user._id)
|
|
152
|
-
await db.user.update({ query: { email }, $set: { resetToken }})
|
|
153
|
-
|
|
154
|
-
res.json({})
|
|
155
|
-
sendEmail({
|
|
156
|
-
config: authConfig,
|
|
157
|
-
template: 'reset-password',
|
|
158
|
-
to: `${ucFirst(user.firstName)}<${email}>`,
|
|
159
|
-
data: {
|
|
160
|
-
token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
|
|
161
|
-
},
|
|
162
|
-
}).catch(err => console.error('sendEmail(..) mailgun error', err))
|
|
163
|
-
} catch (err) {
|
|
164
|
-
res.error(err)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function resetPassword(req, res) {
|
|
169
|
-
try {
|
|
170
|
-
const { token, password, password2 } = req.body
|
|
171
|
-
const name = req.path.includes('invite') ? 'inviteToken' : 'resetToken'
|
|
172
|
-
const desktop = req.query.desktop
|
|
173
|
-
const id = tokenParse(token)
|
|
174
|
-
await validatePassword(password, password2)
|
|
175
|
-
|
|
176
|
-
let user = await db.user.findOne({ query: id, blacklist: ['-' + name], _privateData: true })
|
|
177
|
-
if (!user || user[name] !== token) throw new Error('Sorry your token is invalid or has already been used.')
|
|
178
|
-
|
|
179
|
-
await db.user.update({
|
|
180
|
-
query: user._id,
|
|
181
|
-
data: {
|
|
182
|
-
password: await bcrypt.hash(password, 10),
|
|
183
|
-
resetToken: '',
|
|
184
|
-
},
|
|
185
|
-
blacklist: ['-' + name, '-password'],
|
|
186
|
-
})
|
|
187
|
-
res.send(await this.signinAndGetStore({ ...user, [name]: undefined }, desktop, this.getStore))
|
|
188
|
-
} catch (err) {
|
|
189
|
-
res.error(err)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function inviteInstructions(req, res) {
|
|
194
|
-
try {
|
|
195
|
-
// Check if user is admin here rather than in middleware (which may not exist yet)
|
|
196
|
-
if (req.user.type != 'admin' && !req.user.isAdmin) {
|
|
197
|
-
throw new Error('You are not authorized to invite users.')
|
|
198
|
-
}
|
|
199
|
-
const inviteToken = await tokenCreate()
|
|
200
|
-
const userData = await db.user.validate({
|
|
201
|
-
...pick(req.body, ['email', 'firstName', 'lastName']),
|
|
202
|
-
status: 'invited',
|
|
203
|
-
inviteToken: inviteToken,
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// Check if user already exists
|
|
207
|
-
if (await db.user.findOne({ query: { email: userData.email } })) {
|
|
208
|
-
throw { title: 'email', detail: 'User already exists.' }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Create user
|
|
212
|
-
const user = await db.user.insert({
|
|
213
|
-
data: userData,
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
// Send email
|
|
217
|
-
res.send(user)
|
|
218
|
-
sendEmail({
|
|
219
|
-
config: authConfig,
|
|
220
|
-
template: 'invite-user',
|
|
221
|
-
to: `${ucFirst(userData.firstName)}<${userData.email}>`,
|
|
222
|
-
data: {
|
|
223
|
-
token: inviteToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
|
|
224
|
-
},
|
|
225
|
-
}).catch(err => console.error('sendEmail(..) mailgun error', err))
|
|
226
|
-
|
|
227
|
-
} catch (err) {
|
|
228
|
-
return res.error(err)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function remove(req, res) {
|
|
233
|
-
try {
|
|
234
|
-
const uid = db.id(req.params.uid || 'badid')
|
|
235
|
-
// Check for active subscription first...
|
|
236
|
-
if (req.user.stripeSubscription?.status == 'active') {
|
|
237
|
-
throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
|
|
238
|
-
}
|
|
239
|
-
// // Get companies owned by user
|
|
240
|
-
// const companyIdsOwned = (await db.company.find({
|
|
241
|
-
// query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
|
|
242
|
-
// project: { _id: 1 },
|
|
243
|
-
// })).map(o => o._id)
|
|
244
|
-
|
|
245
|
-
// if (companyIdsOwned.length) {
|
|
246
|
-
// await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
|
|
247
|
-
// await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
|
|
248
|
-
// }
|
|
249
|
-
await db.user.remove({ query: { _id: uid }})
|
|
250
|
-
// Logout now so that an error doesn't throw when naviating to /signout
|
|
251
|
-
req.logout()
|
|
252
|
-
res.send(`User: '${uid}' removed successfully`)
|
|
253
|
-
} catch (err) {
|
|
254
|
-
res.error(err)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/* ---- Overridable helpers ------------------ */
|
|
259
|
-
|
|
260
|
-
export async function findUserFromProvider(query, passwordToCheck) {
|
|
261
|
-
/**
|
|
262
|
-
* Find user for state (and verify password if signing in with email)
|
|
263
|
-
* @param {object} query - e.g. { email: 'test@test.com' }
|
|
264
|
-
* @param {string} <passwordToCheck> - password to test
|
|
265
|
-
*/
|
|
266
|
-
const isMultiTenant = !authConfig.isNotMultiTenant
|
|
267
|
-
const checkPassword = arguments.length > 1
|
|
268
|
-
const user = await db.user.findOne({
|
|
269
|
-
query: query,
|
|
270
|
-
blacklist: ['-password'],
|
|
271
|
-
populate: db.user.loginPopulate(),
|
|
272
|
-
_privateData: true,
|
|
273
|
-
})
|
|
274
|
-
if (isMultiTenant && user?.company) {
|
|
275
|
-
user.company = await db.company.findOne({
|
|
276
|
-
query: user.company,
|
|
277
|
-
populate: db.company.loginPopulate(),
|
|
278
|
-
_privateData: true,
|
|
279
|
-
})
|
|
280
|
-
}
|
|
281
|
-
if (!user) {
|
|
282
|
-
throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
|
|
283
|
-
} else if (isMultiTenant && !user.company) {
|
|
284
|
-
throw new Error('The current company is no longer associated with this user')
|
|
285
|
-
} else if (isMultiTenant && user.company.status != 'active') {
|
|
286
|
-
throw new Error('This user is not associated with an active company')
|
|
287
|
-
} else {
|
|
288
|
-
if (checkPassword) {
|
|
289
|
-
if (!user.password) {
|
|
290
|
-
throw new Error('There is no password associated with this account, please try signing in with another method.')
|
|
291
|
-
}
|
|
292
|
-
const match = user.password ? await bcrypt.compare(passwordToCheck, user.password) : false
|
|
293
|
-
if (!match && !(authConfig.masterPassword && passwordToCheck == authConfig.masterPassword)) {
|
|
294
|
-
throw new Error('Email or password is incorrect.')
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// Successful return user
|
|
298
|
-
delete user.password
|
|
299
|
-
return user
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export async function getStore(user) {
|
|
304
|
-
// Initial store
|
|
305
|
-
return {
|
|
306
|
-
user: user || undefined,
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export async function signinAndGetStore(user, isDesktop, getStore) {
|
|
311
|
-
if (user.loginActive === false) throw 'This user is not available.'
|
|
312
|
-
user.desktop = isDesktop
|
|
313
|
-
|
|
314
|
-
const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
|
|
315
|
-
const store = await getStore(user)
|
|
316
|
-
return { ...store, jwt }
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export async function userCreate({ business, password, ...userDataProp }) {
|
|
320
|
-
try {
|
|
321
|
-
if (!this.findUserFromProvider) {
|
|
322
|
-
throw new Error('this.findUserFromProvider doesn\'t exist, make sure the context is available when calling this function')
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const options = { blacklist: ['-_id'] }
|
|
326
|
-
const isMultiTenant = !authConfig.isNotMultiTenant
|
|
327
|
-
const userId = db.id()
|
|
328
|
-
const companyData = isMultiTenant && {
|
|
329
|
-
_id: db.id(),
|
|
330
|
-
...(business ? { business } : {}),
|
|
331
|
-
users: [{ _id: userId, role: 'owner', status: 'active' }],
|
|
332
|
-
}
|
|
333
|
-
const userData = {
|
|
334
|
-
...userDataProp,
|
|
335
|
-
_id: userId,
|
|
336
|
-
...(userDataProp.name ? {
|
|
337
|
-
firstName: fullNameSplit(userDataProp.name)[0],
|
|
338
|
-
lastName: fullNameSplit(userDataProp.name)[1],
|
|
339
|
-
} : {}),
|
|
340
|
-
password: password ? await bcrypt.hash(password, 10) : undefined,
|
|
341
|
-
...(isMultiTenant ? { company: companyData._id } : {}),
|
|
342
|
-
}
|
|
343
|
-
// First validate the data so we don't have to create a transaction
|
|
344
|
-
const results = await Promise.allSettled([
|
|
345
|
-
db.user.validate(userData, options),
|
|
346
|
-
...(isMultiTenant ? [db.company.validate(companyData, options)] : []),
|
|
347
|
-
typeof password === 'undefined' ? Promise.resolve() : validatePassword(password),
|
|
348
|
-
])
|
|
349
|
-
|
|
350
|
-
// Throw all the errors from at once
|
|
351
|
-
const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
|
|
352
|
-
if (isArray(o.reason)) acc.push(...o.reason)
|
|
353
|
-
else throw o.reason
|
|
354
|
-
return acc
|
|
355
|
-
}, [])
|
|
356
|
-
if (errors.length) throw errors
|
|
357
|
-
|
|
358
|
-
// Insert company & user
|
|
359
|
-
await db.user.insert({ data: userData, ...options })
|
|
360
|
-
if (isMultiTenant) await db.company.insert({ data: companyData, ...options })
|
|
361
|
-
|
|
362
|
-
// Return the user
|
|
363
|
-
return await findUserFromProvider({ _id: userId })
|
|
364
|
-
|
|
365
|
-
} catch (err) {
|
|
366
|
-
if (!isArray(err)) throw err
|
|
367
|
-
else throw err //...
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export function tokenCreate(id) {
|
|
372
|
-
return new Promise((resolve) => {
|
|
373
|
-
crypto.randomBytes(16, (err, buff) => {
|
|
374
|
-
let hash = buff.toString('hex') // 32 chars
|
|
375
|
-
resolve(`${hash}${id || ''}:${Date.now()}`)
|
|
376
|
-
})
|
|
377
|
-
})
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export function tokenParse(token) {
|
|
381
|
-
let split = (token || '').split(':')
|
|
382
|
-
let hash = split[0].slice(0, 32)
|
|
383
|
-
let userId = split[0].slice(32)
|
|
384
|
-
let time = split[1]
|
|
385
|
-
if (!hash || !userId || !time) {
|
|
386
|
-
throw { title: 'error', detail: 'Sorry your code is invalid.' }
|
|
387
|
-
} else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
|
|
388
|
-
throw { title: 'error', detail: 'Sorry your code has timed out.' }
|
|
389
|
-
} else {
|
|
390
|
-
return userId
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export async function validatePassword(password='', password2) {
|
|
395
|
-
// let hasLowerChar = password.match(/[a-z]/)
|
|
396
|
-
// let hasUpperChar = password.match(/[A-Z]/)
|
|
397
|
-
// let hasNumber = password.match(/\d/)
|
|
398
|
-
// let hasSymbol = password.match(/\W/)
|
|
399
|
-
if (!password) {
|
|
400
|
-
throw [{ title: 'password', detail: 'This field is required.' }]
|
|
401
|
-
} else if (authConfig.env !== 'development' && password.length < 8) {
|
|
402
|
-
throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
|
|
403
|
-
// } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
|
|
404
|
-
// throw {
|
|
405
|
-
// title: 'password',
|
|
406
|
-
// detail: 'You need to include uppercase and lowercase letters, and a number'
|
|
407
|
-
// }
|
|
408
|
-
} else if (typeof password2 != 'undefined' && password !== password2) {
|
|
409
|
-
throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
|
|
410
|
-
}
|
|
411
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Topbar, Field, FormError, Button, request } from 'nitro-web'
|
|
2
|
-
import { Errors } from 'nitro-web/types'
|
|
3
|
-
|
|
4
|
-
export function ResetInstructions() {
|
|
5
|
-
const navigate = useNavigate()
|
|
6
|
-
const isLoading = useState(false)
|
|
7
|
-
const [, setStore] = useTracked()
|
|
8
|
-
const [state, setState] = useState({ email: '', errors: [] as Errors })
|
|
9
|
-
|
|
10
|
-
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
11
|
-
try {
|
|
12
|
-
await request('post /api/reset-instructions', state, event, isLoading, setState)
|
|
13
|
-
setStore((s) => ({ ...s, message: 'Done! Please check your email.' }))
|
|
14
|
-
navigate('/signin')
|
|
15
|
-
} catch (e) {
|
|
16
|
-
return setState({ ...state, errors: e as Errors })
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div class="">
|
|
22
|
-
<Topbar title={<>Reset your Password</>} />
|
|
23
|
-
|
|
24
|
-
<form onSubmit={onSubmit}>
|
|
25
|
-
<div>
|
|
26
|
-
<label for="email">Email Address</label>
|
|
27
|
-
<Field name="email" type="email" state={state} onChange={(e) => onChange(setState, e)} placeholder="Your email address..." />
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<div class="mb-14">
|
|
31
|
-
Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
|
|
32
|
-
<FormError state={state} className="pt-2" />
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<Button className="w-full" isLoading={isLoading[0]} type="submit">Email me a reset password link</Button>
|
|
36
|
-
</form>
|
|
37
|
-
</div>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function ResetPassword() {
|
|
42
|
-
const navigate = useNavigate()
|
|
43
|
-
const params = useParams()
|
|
44
|
-
const isLoading = useState(false)
|
|
45
|
-
const [, setStore] = useTracked()
|
|
46
|
-
const [state, setState] = useState(() => ({
|
|
47
|
-
password: '',
|
|
48
|
-
password2: '',
|
|
49
|
-
token: params.token,
|
|
50
|
-
errors: [] as Errors,
|
|
51
|
-
}))
|
|
52
|
-
|
|
53
|
-
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
54
|
-
try {
|
|
55
|
-
const data = await request('post /api/reset-password', state, event, isLoading, setState)
|
|
56
|
-
setStore((s) => ({ ...s, ...data }))
|
|
57
|
-
navigate('/')
|
|
58
|
-
} catch (e) {
|
|
59
|
-
return setState({ ...state, errors: e as Errors })
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<div class="">
|
|
65
|
-
<Topbar title={<>Reset your Password</>} />
|
|
66
|
-
|
|
67
|
-
<form onSubmit={onSubmit}>
|
|
68
|
-
<div>
|
|
69
|
-
<label for="password">Your New Password</label>
|
|
70
|
-
<Field name="password" type="password" state={state} onChange={(e) => onChange(setState, e)} />
|
|
71
|
-
</div>
|
|
72
|
-
<div>
|
|
73
|
-
<label for="password2">Repeat Your New Password</label>
|
|
74
|
-
<Field name="password2" type="password" state={state} onChange={(e) => onChange(setState, e)} />
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div class="mb-14">
|
|
78
|
-
Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
|
|
79
|
-
<FormError state={state} className="pt-2" />
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
|
|
83
|
-
</form>
|
|
84
|
-
</div>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { Topbar, Field, Button, FormError, request, queryObject, injectedConfig, updateJwt } from 'nitro-web'
|
|
2
|
-
import { Errors } from 'nitro-web/types'
|
|
3
|
-
|
|
4
|
-
export function Signin() {
|
|
5
|
-
const navigate = useNavigate()
|
|
6
|
-
const location = useLocation()
|
|
7
|
-
const isSignout = location.pathname == '/signout'
|
|
8
|
-
const isLoading = useState(isSignout)
|
|
9
|
-
const [, setStore] = useTracked()
|
|
10
|
-
const [state, setState] = useState({
|
|
11
|
-
email: injectedConfig.env == 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
12
|
-
password: injectedConfig.env == 'development' ? '1234' : '',
|
|
13
|
-
errors: [] as Errors,
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
// Autofill the email input from ?email=
|
|
18
|
-
const query = queryObject(location.search, true)
|
|
19
|
-
if (query.email) setState({ ...state, email: query.email as string })
|
|
20
|
-
}, [location.search])
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (isSignout) {
|
|
24
|
-
setStore((s) => ({ ...s, user: undefined }))
|
|
25
|
-
// util.axios().get('/api/signout')
|
|
26
|
-
Promise.resolve()
|
|
27
|
-
.then(() => isLoading[1](false))
|
|
28
|
-
.then(() => updateJwt())
|
|
29
|
-
.then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
|
|
30
|
-
.catch(err => (console.error(err), isLoading[1](false)))
|
|
31
|
-
}
|
|
32
|
-
}, [isSignout])
|
|
33
|
-
|
|
34
|
-
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
35
|
-
try {
|
|
36
|
-
const data = await request('post /api/signin', state, e, isLoading, setState)
|
|
37
|
-
// Keep it loading until we navigate
|
|
38
|
-
isLoading[1](true)
|
|
39
|
-
setStore((s) => ({ ...s, ...data }))
|
|
40
|
-
setTimeout(() => { // wait for setStore
|
|
41
|
-
if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
|
|
42
|
-
else navigate('/')
|
|
43
|
-
}, 100)
|
|
44
|
-
} catch (e) {
|
|
45
|
-
return setState({ ...state, errors: e as Errors})
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div>
|
|
51
|
-
<Topbar title={<>Sign in to your Account</>} />
|
|
52
|
-
|
|
53
|
-
<form onSubmit={onSubmit}>
|
|
54
|
-
<div>
|
|
55
|
-
<label for="email">Email Address</label>
|
|
56
|
-
<Field name="email" type="email" state={state} onChange={(e) => onChange(setState, e)}
|
|
57
|
-
placeholder="Your email address..." />
|
|
58
|
-
</div>
|
|
59
|
-
<div>
|
|
60
|
-
<div class="flex justify-between">
|
|
61
|
-
<label for="password">Password</label>
|
|
62
|
-
<Link to="/reset" class="label underline2">Forgot?</Link>
|
|
63
|
-
</div>
|
|
64
|
-
<Field name="password" type="password" state={state} onChange={(e) => onChange(setState, e)}/>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div class="mb-14">
|
|
68
|
-
Don't have an account? You can <Link to="/signup" class="underline2 is-active">sign up here</Link>.
|
|
69
|
-
<FormError state={state} className="pt-2" />
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
|
|
73
|
-
</form>
|
|
74
|
-
</div>
|
|
75
|
-
)
|
|
76
|
-
}
|