nitro-web 0.0.56 → 0.0.58

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.
@@ -1,5 +1,5 @@
1
- // @ts-nocheck
2
1
  import crypto from 'crypto'
2
+ import bcrypt from 'bcrypt'
3
3
  import passport from 'passport'
4
4
  import passportLocal from 'passport-local'
5
5
  import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
@@ -8,7 +8,7 @@ import jsonwebtoken from 'jsonwebtoken'
8
8
  import { sendEmail } from 'nitro-web/server'
9
9
  import { isArray, pick, isString, ucFirst, fullNameSplit } from 'nitro-web/util'
10
10
 
11
- let config = {}
11
+ let authConfig = null
12
12
  const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
13
13
 
14
14
  export default {
@@ -34,10 +34,10 @@ export default {
34
34
  function setup(middleware, _config) {
35
35
  // Setup is called automatically when the server starts
36
36
  // Set config values
37
- const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword']
38
- config = pick(_config, configKeys)
37
+ const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant']
38
+ authConfig = pick(_config, configKeys)
39
39
  for (const key of ['clientUrl', 'emailFrom', 'env', 'name']) {
40
- if (!config[key]) throw new Error(`Missing config value for: config.${key}`)
40
+ if (!authConfig[key]) throw new Error(`Missing config value for: config.${key}`)
41
41
  }
42
42
 
43
43
  passport.use(
@@ -104,7 +104,7 @@ async function signup(req, res) {
104
104
  try {
105
105
  let user = await userCreate(req.body)
106
106
  sendEmail({
107
- config: config,
107
+ config: authConfig,
108
108
  template: 'welcome',
109
109
  to: `${ucFirst(user.firstName)}<${user.email}>`,
110
110
  }).catch(console.error)
@@ -147,7 +147,7 @@ async function resetInstructions(req, res) {
147
147
 
148
148
  res.json({})
149
149
  sendEmail({
150
- config: config,
150
+ config: authConfig,
151
151
  template: 'reset-password',
152
152
  to: `${ucFirst(user.firstName)}<${email}>`,
153
153
  data: {
@@ -171,7 +171,7 @@ async function resetPassword(req, res) {
171
171
  await db.user.update({
172
172
  query: user._id,
173
173
  data: {
174
- password: await (await import('bcrypt')).hash(password, 10),
174
+ password: await bcrypt.hash(password, 10),
175
175
  resetToken: '',
176
176
  },
177
177
  blacklist: ['-resetToken', '-password'],
@@ -185,39 +185,75 @@ async function resetPassword(req, res) {
185
185
  async function remove(req, res) {
186
186
  try {
187
187
  const uid = db.id(req.params.uid || 'badid')
188
-
189
- // Get companies owned by user
190
- const companyIdsOwned = (await db.company.find({
191
- query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
192
- project: { _id: 1 },
193
- })).map(o => o._id)
194
-
195
188
  // Check for active subscription first...
196
189
  if (req.user.stripeSubscription?.status == 'active') {
197
190
  throw { title: 'subscription', detail: 'You need to cancel your subscription first.' }
198
191
  }
192
+ // // Get companies owned by user
193
+ // const companyIdsOwned = (await db.company.find({
194
+ // query: { users: { $elemMatch: { _id: uid, role: 'owner' } } },
195
+ // project: { _id: 1 },
196
+ // })).map(o => o._id)
199
197
 
200
- if (companyIdsOwned.length) {
201
- await db.transaction.remove({ query: { company: { $in: companyIdsOwned }}})
202
- await db.statement.remove({ query: { company: { $in: companyIdsOwned }}})
203
- await db.account.remove({ query: { company: { $in: companyIdsOwned }}})
204
- await db.document.remove({ query: { company: { $in: companyIdsOwned }}})
205
- await db.contact.remove({ query: { company: { $in: companyIdsOwned }}})
206
- await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
207
- await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
208
- }
198
+ // if (companyIdsOwned.length) {
199
+ // await db.product.remove({ query: { company: { $in: companyIdsOwned }}})
200
+ // await db.company.remove({ query: { _id: { $in: companyIdsOwned }}})
201
+ // }
209
202
  await db.user.remove({ query: { _id: uid }})
210
203
  // Logout now so that an error doesn't throw when naviating to /signout
211
204
  req.logout()
212
- res.send(`User: '${uid}' and companies: '${companyIdsOwned.join(', ')}' removed successfully`)
205
+ res.send(`User: '${uid}' removed successfully`)
213
206
  } catch (err) {
214
207
  res.error(err)
215
208
  }
216
209
  }
217
210
 
218
- /* ---- Private fns ---------------- */
211
+ /* ---- Auth helpers ------------------------- */
219
212
 
220
- async function getStore(user) {
213
+ export async function findUserFromProvider(query, passwordToCheck) {
214
+ /**
215
+ * Find user for state (and verify password if signing in with email)
216
+ * @param {object} query - e.g. { email: 'test@test.com' }
217
+ * @param {string} <passwordToCheck> - password to test
218
+ */
219
+ const isMultiTenant = !authConfig.isNotMultiTenant
220
+ const checkPassword = arguments.length > 1
221
+ const user = await db.user.findOne({
222
+ query: query,
223
+ blacklist: ['-password'],
224
+ populate: db.user.loginPopulate(),
225
+ _privateData: true,
226
+ })
227
+ if (isMultiTenant && user?.company) {
228
+ user.company = await db.company.findOne({
229
+ query: user.company,
230
+ populate: db.company.loginPopulate(),
231
+ _privateData: true,
232
+ })
233
+ }
234
+ if (!user) {
235
+ throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
236
+ } else if (isMultiTenant && !user.company) {
237
+ throw new Error('The current company is no longer associated with this user')
238
+ } else if (isMultiTenant && user.company.status != 'active') {
239
+ throw new Error('This user is not associated with an active company')
240
+ } else {
241
+ if (checkPassword) {
242
+ if (!user.password) {
243
+ throw new Error('There is no password associated with this account, please try signing in with another method.')
244
+ }
245
+ const match = user.password ? await bcrypt.compare(passwordToCheck, user.password) : false
246
+ if (!match && !(authConfig.masterPassword && passwordToCheck == authConfig.masterPassword)) {
247
+ throw new Error('Email or password is incorrect.')
248
+ }
249
+ }
250
+ // Successful return user
251
+ delete user.password
252
+ return user
253
+ }
254
+ }
255
+
256
+ export async function getStore(user) {
221
257
  // Initial store
222
258
  return {
223
259
  user: user || undefined,
@@ -256,47 +292,29 @@ export function tokenParse(token) {
256
292
  }
257
293
  }
258
294
 
259
- async function validatePassword(password='', password2) {
260
- // let hasLowerChar = password.match(/[a-z]/)
261
- // let hasUpperChar = password.match(/[A-Z]/)
262
- // let hasNumber = password.match(/\d/)
263
- // let hasSymbol = password.match(/\W/)
264
- if (!password) {
265
- throw [{ title: 'password', detail: 'This field is required.' }]
266
- } else if (config.env !== 'development' && password.length < 8) {
267
- throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
268
- // } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
269
- // throw {
270
- // title: 'password',
271
- // detail: 'You need to include uppercase and lowercase letters, and a number'
272
- // }
273
- } else if (typeof password2 != 'undefined' && password !== password2) {
274
- throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
275
- }
276
- }
277
-
278
295
  export async function userCreate({ name, business, email, password }) {
279
296
  try {
280
297
  const options = { blacklist: ['-_id'] }
298
+ const isMultiTenant = !authConfig.isNotMultiTenant
281
299
  const userId = db.id()
282
- const companyData = {
300
+ const companyData = isMultiTenant && {
283
301
  _id: db.id(),
284
302
  ...(business ? { business } : {}),
285
303
  users: [{ _id: userId, role: 'owner', status: 'active' }],
286
304
  }
287
305
  const userData = {
288
306
  _id: userId,
289
- company: companyData._id,
290
307
  email: email,
291
308
  firstName: fullNameSplit(name)[0],
292
309
  lastName: fullNameSplit(name)[1],
293
- password: password ? await (await import('bcrypt')).hash(password, 10) : undefined,
310
+ password: password ? await bcrypt.hash(password, 10) : undefined,
311
+ ...(isMultiTenant ? { company: companyData._id } : {}),
294
312
  }
295
313
 
296
314
  // First validate the data so we don't have to create a transaction
297
315
  const results = await Promise.allSettled([
298
316
  db.user.validate(userData, options),
299
- db.company.validate(companyData, options),
317
+ ...(isMultiTenant ? [db.company.validate(companyData, options)] : []),
300
318
  typeof password === 'undefined' ? Promise.resolve() : validatePassword(password),
301
319
  ])
302
320
 
@@ -310,7 +328,7 @@ export async function userCreate({ name, business, email, password }) {
310
328
 
311
329
  // Insert company & user
312
330
  await db.user.insert({ data: userData, ...options })
313
- await db.company.insert({ data: companyData, ...options })
331
+ if (isMultiTenant) await db.company.insert({ data: companyData, ...options })
314
332
 
315
333
  // Return the user
316
334
  return await findUserFromProvider({ _id: userId })
@@ -324,45 +342,21 @@ export async function userCreate({ name, business, email, password }) {
324
342
  }
325
343
  }
326
344
 
327
- export async function findUserFromProvider(query, passwordToCheck) {
328
- /**
329
- * Find user for state (and verify password if signing in with email)
330
- * @param {object} query - e.g. { email: 'test@test.com' }
331
- * @param {string} <passwordToCheck> - password to test
332
- */
333
- const checkPassword = arguments.length > 1
334
- const user = await db.user.findOne({
335
- query: query,
336
- blacklist: ['-password'],
337
- populate: db.user.loginPopulate(),
338
- _privateData: true,
339
- })
340
- if (user?.company) {
341
- user.company = await db.company.findOne({
342
- query: user.company,
343
- populate: db.company.loginPopulate(),
344
- _privateData: true,
345
- })
346
- }
347
- if (!user) {
348
- throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
349
- } else if (!user.company) {
350
- throw new Error('The current company is no longer associated with this user')
351
- } else if (user.company.status != 'active') {
352
- throw new Error('This user is not associated with an active company')
353
- } else {
354
- if (checkPassword) {
355
- if (!user.password) {
356
- throw new Error('There is no password associated with this account, please try signing in with another method.')
357
- }
358
- const match = user.password ? await (await import('bcrypt')).compare(passwordToCheck, user.password) : false
359
- if (!match && !(config.masterPassword && passwordToCheck == config.masterPassword)) {
360
- throw new Error('Email or password is incorrect.')
361
- }
362
- }
363
- // Successful return user
364
- delete user.password
365
- return user
345
+ export async function validatePassword(password='', password2) {
346
+ // let hasLowerChar = password.match(/[a-z]/)
347
+ // let hasUpperChar = password.match(/[A-Z]/)
348
+ // let hasNumber = password.match(/\d/)
349
+ // let hasSymbol = password.match(/\W/)
350
+ if (!password) {
351
+ throw [{ title: 'password', detail: 'This field is required.' }]
352
+ } else if (authConfig.env !== 'development' && password.length < 8) {
353
+ throw [{ title: 'password', detail: 'Your password needs to be atleast 8 characters long' }]
354
+ // } else if (!hasLowerChar || !hasUpperChar || !hasNumber || !hasSymbol) {
355
+ // throw {
356
+ // title: 'password',
357
+ // detail: 'You need to include uppercase and lowercase letters, and a number'
358
+ // }
359
+ } else if (typeof password2 != 'undefined' && password !== password2) {
360
+ throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
366
361
  }
367
362
  }
368
-
@@ -2,13 +2,14 @@
2
2
  import { isObject, isString, queryObject } from 'nitro-web/util'
3
3
  import { X, CircleCheck } from 'lucide-react'
4
4
  import { MessageObject } from 'nitro-web/types'
5
+ import { twMerge } from 'nitro-web'
5
6
 
6
7
  /**
7
8
  * Shows a message
8
9
  * Triggered by navigating to a link with a valid query string, or
9
10
  * by setting store.message to a string or more explicitly, to an object
10
11
  **/
11
- export function Message() {
12
+ export function Message({ className }: { className?: string }) {
12
13
  const devDontHide = false
13
14
  const [store, setStore] = useTracked()
14
15
  const [visible, setVisible] = useState(false)
@@ -81,13 +82,13 @@ export function Message() {
81
82
  setVisible(false)
82
83
  setTimeout(() => setStore(s => ({ ...s, message: undefined })), 250)
83
84
  }
84
-
85
+
85
86
  return (
86
87
  <>
87
88
  {/* Global notification live region, render this permanently at the end of the document */}
88
89
  <div
89
90
  aria-live="assertive"
90
- className="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-20 nitro-message"
91
+ className={`${twMerge(`pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-[101] ${className||''}`)} nitro-message`}
91
92
  >
92
93
  <div className="flex w-full flex-col items-center space-y-4 sm:items-end">
93
94
  {isObject(store.message) && (
@@ -1,4 +1,4 @@
1
- import { IsFirstRender } from 'nitro-web'
1
+ import { IsFirstRender, twMerge } from 'nitro-web'
2
2
  import SvgX1 from 'nitro-web/client/imgs/icons/x1.svg'
3
3
 
4
4
  type ModalProps = {
@@ -64,7 +64,7 @@ export function Modal({ show, setShow, children, maxWidth, minHeight, dismissabl
64
64
  return (
65
65
  <div
66
66
  onClick={(e) => e.stopPropagation()}
67
- class={`fixed top-0 w-[100vw] h-[100vh] z-[700] nitro-modal ${_state.root}`}
67
+ class={`${twMerge(`fixed top-0 w-[100vw] h-[100vh] z-[100] ${_state.root} ${props.className}`)} nitro-modal`}
68
68
  >
69
69
  <div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${_state.bg}`}></div>
70
70
  <div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${_state.container}`}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "repository": "github:boycce/nitro-web",
5
5
  "homepage": "https://boycce.github.io/nitro-web/",
6
6
  "description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
package/server/index.js CHANGED
@@ -19,6 +19,15 @@ export { setupRouter } from './router.js'
19
19
  export { sendEmail } from './email/index.js'
20
20
 
21
21
  // Export api default controllers
22
- export { default as auth, findUserFromProvider, signinAndGetState, userCreate } from '../components/auth/auth.api.js'
22
+ export {
23
+ default as auth,
24
+ findUserFromProvider,
25
+ signinAndGetState,
26
+ userCreate,
27
+ tokenCreate,
28
+ tokenParse,
29
+ validatePassword,
30
+ getStore,
31
+ } from '../components/auth/auth.api.js'
23
32
  export { default as settings } from '../components/settings/settings.api.js'
24
33
  export { default as stripe } from '../components/billing/stripe.api.js'