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.
- package/.editorconfig +9 -0
- package/.eslintrc.json +86 -0
- package/_example/.env-example +16 -0
- package/_example/client/config.ts +5 -0
- package/_example/client/css/index.css +35 -0
- package/_example/client/fonts/Roboto-Bold.ttf +0 -0
- package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Italic.ttf +0 -0
- package/_example/client/fonts/Roboto-Medium.ttf +0 -0
- package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Regular.ttf +0 -0
- package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
- package/_example/client/imgs/android-chrome-512x512.png +0 -0
- package/_example/client/imgs/favicon.png +0 -0
- package/_example/client/imgs/icons/calendar.svg +3 -0
- package/_example/client/imgs/icons/email.svg +6 -0
- package/_example/client/imgs/icons/eye-open.svg +4 -0
- package/_example/client/imgs/icons/eye.svg +5 -0
- package/_example/client/imgs/icons/filter.svg +7 -0
- package/_example/client/imgs/icons/left-circle.svg +3 -0
- package/_example/client/imgs/icons/left.svg +3 -0
- package/_example/client/imgs/icons/line-options.svg +5 -0
- package/_example/client/imgs/icons/line.svg +3 -0
- package/_example/client/imgs/icons/person.svg +7 -0
- package/_example/client/imgs/icons/plus-circle.svg +5 -0
- package/_example/client/imgs/icons/plus.svg +5 -0
- package/_example/client/imgs/icons/right-circle.svg +3 -0
- package/_example/client/imgs/icons/right.svg +3 -0
- package/_example/client/imgs/icons/search.svg +3 -0
- package/_example/client/imgs/icons/shield.svg +6 -0
- package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/_example/client/imgs/icons/tick-circle.svg +6 -0
- package/_example/client/imgs/icons/tick.svg +5 -0
- package/_example/client/imgs/icons/up2-small.svg +4 -0
- package/_example/client/imgs/icons/up2.svg +4 -0
- package/_example/client/imgs/icons/updown.svg +6 -0
- package/_example/client/imgs/icons/v-big-dark.svg +3 -0
- package/_example/client/imgs/icons/v-dark.svg +3 -0
- package/_example/client/imgs/icons/v.svg +3 -0
- package/_example/client/imgs/icons/v2-active.svg +6 -0
- package/_example/client/imgs/icons/x1.svg +4 -0
- package/_example/client/imgs/logo/logo-white.svg +20 -0
- package/_example/client/imgs/logo/logo.svg +20 -0
- package/_example/client/imgs/no-image.jpg +0 -0
- package/_example/client/imgs/user.jpg +0 -0
- package/_example/client/index.html +12 -0
- package/_example/client/index.ts +47 -0
- package/_example/components/auth.api.js +1 -0
- package/_example/components/index.tsx +225 -0
- package/_example/components/partials/layouts.tsx +5 -0
- package/_example/components/settings.api.js +1 -0
- package/_example/server/config.js +120 -0
- package/_example/server/email/welcome.html +27 -0
- package/_example/server/index.js +32 -0
- package/_example/tailwind.config.js +84 -0
- package/_example/tsconfig.json +32 -0
- package/_example/types.d.ts +7 -0
- package/_example/webpack.config.js +4 -0
- package/client/app.js +300 -0
- package/client/css/components.css +84 -0
- package/client/css/fonts.css +67 -0
- package/client/imgs/icons/calendar.svg +3 -0
- package/client/imgs/icons/email.svg +6 -0
- package/client/imgs/icons/eye-open.svg +4 -0
- package/client/imgs/icons/eye.svg +5 -0
- package/client/imgs/icons/filter.svg +7 -0
- package/client/imgs/icons/left-circle.svg +3 -0
- package/client/imgs/icons/left.svg +3 -0
- package/client/imgs/icons/line-options.svg +5 -0
- package/client/imgs/icons/line.svg +3 -0
- package/client/imgs/icons/person.svg +7 -0
- package/client/imgs/icons/plus-circle.svg +5 -0
- package/client/imgs/icons/plus.svg +5 -0
- package/client/imgs/icons/right-circle.svg +3 -0
- package/client/imgs/icons/right.svg +3 -0
- package/client/imgs/icons/search.svg +3 -0
- package/client/imgs/icons/shield.svg +6 -0
- package/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/client/imgs/icons/tick-circle.svg +6 -0
- package/client/imgs/icons/tick.svg +5 -0
- package/client/imgs/icons/up2-small.svg +4 -0
- package/client/imgs/icons/up2.svg +4 -0
- package/client/imgs/icons/updown.svg +6 -0
- package/client/imgs/icons/v-big-dark.svg +3 -0
- package/client/imgs/icons/v-dark.svg +3 -0
- package/client/imgs/icons/v.svg +3 -0
- package/client/imgs/icons/v2-active.svg +6 -0
- package/client/imgs/icons/x1.svg +4 -0
- package/client.js +42 -0
- package/components/auth/auth.api.js +419 -0
- package/components/auth/reset.jsx +88 -0
- package/components/auth/signin.jsx +74 -0
- package/components/auth/signup.jsx +62 -0
- package/components/billing/stripe.api.js +267 -0
- package/components/partials/element/accordion.jsx +82 -0
- package/components/partials/element/avatar.jsx +28 -0
- package/components/partials/element/button.jsx +66 -0
- package/components/partials/element/dropdown.jsx +185 -0
- package/components/partials/element/initials.jsx +56 -0
- package/components/partials/element/message.jsx +124 -0
- package/components/partials/element/modal.jsx +229 -0
- package/components/partials/element/sidebar.jsx +166 -0
- package/components/partials/element/tooltip.jsx +146 -0
- package/components/partials/element/topbar.jsx +25 -0
- package/components/partials/form/checkbox.jsx +74 -0
- package/components/partials/form/drop-handler.jsx +62 -0
- package/components/partials/form/drop.jsx +125 -0
- package/components/partials/form/form-error.jsx +21 -0
- package/components/partials/form/input-color.jsx +77 -0
- package/components/partials/form/input-currency.jsx +133 -0
- package/components/partials/form/input-date.jsx +223 -0
- package/components/partials/form/input.jsx +131 -0
- package/components/partials/form/location.jsx +212 -0
- package/components/partials/form/select.jsx +369 -0
- package/components/partials/form/toggle.jsx +46 -0
- package/components/partials/is-first-render.js +15 -0
- package/components/partials/layout/layout1.jsx +32 -0
- package/components/partials/layout/layout2.jsx +47 -0
- package/components/partials/not-found.jsx +7 -0
- package/components/partials/styleguide.jsx +252 -0
- package/components/settings/settings-account.jsx +143 -0
- package/components/settings/settings-business.jsx +121 -0
- package/components/settings/settings-team--member.jsx +108 -0
- package/components/settings/settings-team.jsx +76 -0
- package/components/settings/settings.api.js +54 -0
- package/package.json +175 -0
- package/readme.md +43 -0
- package/server/email/index.js +192 -0
- package/server/email/partials/email.css +153 -0
- package/server/email/partials/layout1.swig +92 -0
- package/server/email/partials/line.swig +8 -0
- package/server/email/partials/vert-10.swig +8 -0
- package/server/email/partials/vert-15.swig +8 -0
- package/server/email/partials/vert-20.swig +8 -0
- package/server/email/partials/vert-25.swig +8 -0
- package/server/email/partials/vert-30.swig +8 -0
- package/server/email/partials/vert-35.swig +8 -0
- package/server/email/partials/vert-50.swig +8 -0
- package/server/email/reset-password.html +21 -0
- package/server/email/welcome.html +21 -0
- package/server/models/company.js +76 -0
- package/server/models/user.js +45 -0
- package/server/router.js +355 -0
- package/server.js +20 -0
- package/util.js +1145 -0
- 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'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
|
+
}
|