nitro-web 0.0.33 → 0.0.35

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.
@@ -6,7 +6,7 @@ import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
6
6
  import db from 'monastery'
7
7
  import jsonwebtoken from 'jsonwebtoken'
8
8
  import { sendEmail } from 'nitro-web/server'
9
- import * as util from 'nitro-web/util'
9
+ import { isArray, pick, isString, ucFirst, fullNameSplit } from 'nitro-web/util'
10
10
 
11
11
  let config = {}
12
12
  const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
@@ -32,9 +32,13 @@ export default {
32
32
  }
33
33
 
34
34
  function setup(middleware, _config) {
35
+ // Setup is called automatically when the server starts
35
36
  // Set config values
36
- config = { env: _config.env, masterPassword: _config.masterPassword }
37
- if (!config.env) throw new Error('Missing config value for: config.env')
37
+ const configKeys = ['clientUrl', 'emailFrom', 'env', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'name']
38
+ config = pick(_config, configKeys)
39
+ for (const key of configKeys) {
40
+ if (!config[key]) throw new Error(`Missing config value for: config.${key}`)
41
+ }
38
42
 
39
43
  passport.use(
40
44
  new passportLocal.Strategy(
@@ -102,7 +106,7 @@ async function signup(req, res) {
102
106
  sendEmail({
103
107
  config: config,
104
108
  template: 'welcome',
105
- to: `${util.ucFirst(user.firstName)}<${user.email}>`,
109
+ to: `${ucFirst(user.firstName)}<${user.email}>`,
106
110
  }).catch(console.error)
107
111
  res.send(await signinAndGetState(user, req.query.desktop))
108
112
  } catch (err) {
@@ -133,7 +137,7 @@ function signout(req, res) {
133
137
  async function resetInstructions(req, res) {
134
138
  try {
135
139
  let email = (req.body.email || '').trim().toLowerCase()
136
- if (!email || !util.isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
140
+ if (!email || !isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
137
141
 
138
142
  let user = await db.user.findOne({ query: { email }, _privateData: true })
139
143
  if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
@@ -145,7 +149,7 @@ async function resetInstructions(req, res) {
145
149
  sendEmail({
146
150
  config: config,
147
151
  template: 'reset-password',
148
- to: `${util.ucFirst(user.firstName)}<${email}>`,
152
+ to: `${ucFirst(user.firstName)}<${email}>`,
149
153
  data: {
150
154
  token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
151
155
  },
@@ -271,21 +275,21 @@ async function validatePassword(password='', password2) {
271
275
  }
272
276
  }
273
277
 
274
- export async function userCreate({ name, businessName, email, password }) {
278
+ export async function userCreate({ name, business, email, password }) {
275
279
  try {
276
280
  const options = { blacklist: ['-_id'] }
277
281
  const userId = db.id()
278
282
  const companyData = {
279
283
  _id: db.id(),
280
- business: { name: businessName },
284
+ ...(business ? { business } : {}),
281
285
  users: [{ _id: userId, role: 'owner', status: 'active' }],
282
286
  }
283
287
  const userData = {
284
288
  _id: userId,
285
289
  company: companyData._id,
286
290
  email: email,
287
- firstName: util.fullNameSplit(name)[0],
288
- lastName: util.fullNameSplit(name)[1],
291
+ firstName: fullNameSplit(name)[0],
292
+ lastName: fullNameSplit(name)[1],
289
293
  password: password ? await (await import('bcrypt')).hash(password, 10) : undefined,
290
294
  }
291
295
 
@@ -298,7 +302,7 @@ export async function userCreate({ name, businessName, email, password }) {
298
302
 
299
303
  // Throw all the errors from at once
300
304
  const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
301
- if (util.isArray(o.reason)) acc.push(...o.reason)
305
+ if (isArray(o.reason)) acc.push(...o.reason)
302
306
  else throw o.reason
303
307
  return acc
304
308
  }, [])
@@ -312,7 +316,7 @@ export async function userCreate({ name, businessName, email, password }) {
312
316
  return await findUserFromProvider({ _id: userId })
313
317
 
314
318
  } catch (err) {
315
- if (!util.isArray(err)) throw err
319
+ if (!isArray(err)) throw err
316
320
  throw err.map((o) => {
317
321
  if (o.title == 'firstName') o.title = 'name'
318
322
  return o
@@ -320,13 +324,13 @@ export async function userCreate({ name, businessName, email, password }) {
320
324
  }
321
325
  }
322
326
 
323
- export async function findUserFromProvider(query, passwordToTest) {
327
+ export async function findUserFromProvider(query, passwordToCheck) {
324
328
  /**
325
329
  * Find user for state (and verify password if signing in with email)
326
330
  * @param {object} query - e.g. { email: 'test@test.com' }
327
- * @param {string} <passwordToTest> - password to test
331
+ * @param {string} <passwordToCheck> - password to test
328
332
  */
329
- const testPassword = arguments.length > 1
333
+ const checkPassword = arguments.length > 1
330
334
  const user = await db.user.findOne({
331
335
  query: query,
332
336
  blacklist: ['-password'],
@@ -341,15 +345,18 @@ export async function findUserFromProvider(query, passwordToTest) {
341
345
  })
342
346
  }
343
347
  if (!user) {
344
- throw new Error(testPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
348
+ throw new Error(checkPassword ? 'Email or password is incorrect.' : 'Session-user is invalid.')
345
349
  } else if (!user.company) {
346
350
  throw new Error('The current company is no longer associated with this user')
347
351
  } else if (user.company.status != 'active') {
348
352
  throw new Error('This user is not associated with an active company')
349
353
  } else {
350
- if (testPassword) {
351
- const match = user.password ? await (await import('bcrypt')).compare(passwordToTest, user.password) : false
352
- if (!match && !(config.masterPassword && passwordToTest == config.masterPassword)) {
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)) {
353
360
  throw new Error('Email or password is incorrect.')
354
361
  }
355
362
  }
@@ -3,7 +3,7 @@ import { Errors } from 'nitro-web/types'
3
3
 
4
4
  export function ResetInstructions() {
5
5
  const navigate = useNavigate()
6
- const isLoading = useState()
6
+ const isLoading = useState(false)
7
7
  const [, setStore] = useTracked()
8
8
  const [state, setState] = useState({ email: '', errors: [] as Errors })
9
9
 
@@ -41,7 +41,7 @@ export function ResetInstructions() {
41
41
  export function ResetPassword() {
42
42
  const navigate = useNavigate()
43
43
  const params = useParams()
44
- const isLoading = useState()
44
+ const isLoading = useState(false)
45
45
  const [, setStore] = useTracked()
46
46
  const [state, setState] = useState(() => ({
47
47
  password: '',
@@ -29,16 +29,16 @@ export function Button({
29
29
  const iconPosition = IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : 'none'
30
30
  const base =
31
31
  'relative inline-block text-center font-medium shadow-sm focus-visible:outline focus-visible:outline-2 ' +
32
- 'focus-visible:outline-offset-2 text-white'
32
+ 'focus-visible:outline-offset-2 text-white [&>.loader]:border-white'
33
33
 
34
34
  // Button colors, you can use custom colors by using className instead
35
35
  const colors = {
36
36
  primary: 'bg-primary hover:bg-primary-hover',
37
37
  secondary: 'bg-secondary hover:bg-secondary-hover',
38
38
  black: 'bg-black hover:bg-gray-700',
39
- white: 'bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 [&>.loader]:border-black',
39
+ white: 'bg-white ring-1 ring-inset ring-gray-300 hover:bg-gray-50 text-gray-900 [&>.loader]:border-black',
40
40
  }
41
-
41
+
42
42
  // Button sizes
43
43
  const sizes = {
44
44
  xs: 'px-2 py-1 px-button-x-xs py-button-y-xs text-xs rounded',
@@ -67,7 +67,7 @@ export function Button({
67
67
  isLoading &&
68
68
  <span className={
69
69
  'loader !opacity-100 absolute top-[50%] left-[50%] w-[1rem] h-[1rem] ml-[-0.5rem] mt-[-0.5rem] ' +
70
- 'rounded-full animate-spin border-2 !border-t-transparent border-white'
70
+ 'rounded-full animate-spin border-2 !border-t-transparent'
71
71
  } />
72
72
  }
73
73
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
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,21 +7,21 @@ import inlineCss from 'inline-css'
7
7
  import { dirname, join } from 'path'
8
8
  import { fileURLToPath } from 'url'
9
9
  import path from 'path'
10
- import { getDirectories } from 'nitro-web/util'
10
+ import { getDirectories, ucFirst } from 'nitro-web/util'
11
11
 
12
12
  let templates = {}
13
13
  let nodemailerMailgun = undefined
14
14
  const _dirname = dirname(fileURLToPath(import.meta.url)) + '/'
15
15
 
16
- export async function sendEmail({ template, to, bcc, data={}, from, replyTo, recipientVariables, subject, test, skipCssInline, config }) {
16
+ export async function sendEmail({ template, to, bcc, data, from, replyTo, recipientVariables, subject, test, skipCssInline, config }) {
17
17
  /**
18
18
  * Email recipient a predefined template with data and/or recipientVariables
19
19
  *
20
20
  * @param {string} template = e.g. 'reset-password' or html
21
- * @param {string} to - e.g. "Bruce Lee<bruce@gmail.com>, ..."
22
- * @param {object} config - e.g. { mailgunKey, mailgunDomain, emailFrom, clientUrl }
21
+ * @param {string} to - e.g. "Bruce Wayne<bruce@wayneenterprises.com>, ..."
22
+ * @param {object} config - e.g. { mailgunKey, mailgunDomain, emailFrom, clientUrl, name }
23
23
  * @param {string} <bcc> - e.g. "Chuck Norris<chuck@gmail.com>" (not sent in development)
24
- * @param {object} <data> - recipientVariables[to] shorthand
24
+ * @param {object} <data> - recipientVariables[to] shorthand, data is copied to all recipients
25
25
  * @param {string} <from> - e.g. "Chuck Norris<chuck@gmail.com>"
26
26
  * @param {string} <replyTo> - e.g. "Chuck Norris<chuck@gmail.com>"
27
27
  * @param {object} <recipientVariables> - mailgun recipient-variables for batch sending
@@ -32,8 +32,12 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
32
32
  */
33
33
  if (!config) {
34
34
  throw new Error('sendEmail: `config` missing')
35
- } else if (!config.emailFrom || !config.clientUrl) {
36
- throw new Error('sendEmail: `config.emailFrom` or `config.clientUrl` is missing')
35
+ } else if (!config.emailFrom) {
36
+ throw new Error('sendEmail: `config.emailFrom` is missing')
37
+ } else if (!config.clientUrl) {
38
+ throw new Error('sendEmail: `config.clientUrl` is missing')
39
+ } else if (!config.name) {
40
+ throw new Error('sendEmail: `config.name` is missing')
37
41
  } else if (!test && (!config.mailgunKey || !config.mailgunDomain)) {
38
42
  throw new Error('sendEmail: `config.mailgunKey` or `config.mailgunDomain` is missing')
39
43
  } else if (!template) {
@@ -52,27 +56,30 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
52
56
  // From, replayTo
53
57
  from = from || config.emailFrom
54
58
  replyTo = replyTo || config.emailReplyTo || from
55
-
56
- // Data is recipientVariables[to] shorthand
57
- if (data) {
58
- recipientVariables = { [getNameEmail(to)[1]]: data }
59
- }
59
+ const toSplit = to.split(',')
60
60
 
61
61
  // Add default recipientVariables
62
- for (let toEmail in recipientVariables) {
62
+ if (!recipientVariables) recipientVariables = {}
63
+
64
+ // Add default values to recipientVariables
65
+ for (let toNameEmail of toSplit) {
66
+ const [toName, toEmail] = getNameEmail(toNameEmail)
63
67
  recipientVariables[toEmail] = {
68
+ ...(data || {}),
69
+ configName: ucFirst(config.name),
64
70
  domain: config.clientUrl,
65
- email: toEmail,
66
- greet: data.name || getNameEmail(to)[0]? 'Hi ' + (data.name || getNameEmail(to)[0]) : 'Hello',
67
- name: getNameEmail(to)[0],
68
71
  replyToEmail: getNameEmail(replyTo)[1],
69
72
  replyToName: getNameEmail(replyTo)[0],
70
- ...recipientVariables[toEmail],
73
+ email: toEmail,
74
+ greet: toName ? 'Hi ' + toName : 'Hello',
75
+ name: toName,
76
+ ...(recipientVariables[toEmail] || {}),
71
77
  }
72
78
  }
73
79
 
74
80
  let settings = {
75
81
  bcc: bcc,
82
+ emailTemplateDir: getDirectories(path, config.pwd).emailTemplateDir,
76
83
  from: from,
77
84
  isDev: config.clientUrl.match(/:/), // possibly use config.env here
78
85
  recipientVariables: recipientVariables,
@@ -83,7 +90,6 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
83
90
  test: config.emailTestMode || test,
84
91
  to: to,
85
92
  url: config.clientUrl,
86
- emailTemplateDir: getDirectories(path, config.pwd).emailTemplateDir,
87
93
  }
88
94
 
89
95
  // Grab html and send
@@ -31,7 +31,7 @@
31
31
  <tr>
32
32
  <td width="180px" align="left">
33
33
  <a href="%recipient.domain%" target="_blank">
34
- <img src="%recipient.domain%/assets/imgs/logo/logo-white.svg" width="auto" height="22" border="0">
34
+ <img src="%recipient.domain%/assets/imgs/logo/logo.svg" width="auto" height="22" border="0">
35
35
  </a>
36
36
  </td>
37
37
  <td align="right">
@@ -2,7 +2,7 @@
2
2
  <table width="100%" align="center" cellpadding="0" cellspacing="0" border="0">
3
3
  <tbody>
4
4
  <tr>
5
- <td width="100%" height="28"></td>
5
+ <td width="100%" height="25"></td>
6
6
  </tr>
7
7
  </tbody>
8
8
  </table>
@@ -1,4 +1,13 @@
1
+ <!--
2
+ - You can view templates at http://localhost:3001/email/reset-password
3
+ - To modify the css, add your own /server/email/partials/email.css file
4
+ - You can copy any nitro email .swig/html template to your project, and modify it
5
+ -->
6
+
7
+ <!-- extends references the default nitro layout file -->
1
8
  {% extends "partials/layout1.swig" %}
9
+
10
+ <!-- block content is the content of the email -->
2
11
  {% block content %}
3
12
 
4
13
  [[ subject = Reset your Password ]]
@@ -15,7 +24,6 @@
15
24
  </span><br/>
16
25
  <br/>
17
26
  Thanks,<br/>
18
- <b>The Nitro Team</b>
19
-
27
+ <b>The %recipient.configName% Team</b>
20
28
 
21
29
  {% endblock %}
@@ -1,10 +1,19 @@
1
+ <!--
2
+ - You can view templates at http://localhost:3001/email/welcome
3
+ - To modify the css, add your own /server/email/partials/email.css file
4
+ - You can copy any nitro email .swig/html template to your project, and modify it
5
+ -->
6
+
7
+ <!-- extends references the default nitro layout file -->
1
8
  {% extends "partials/layout1.swig" %}
9
+
10
+ <!-- block content is the content of the email -->
2
11
  {% block content %}
3
12
 
4
- [[ subject = Welcome to Nitro ]]
13
+ [[ subject = Welcome to %recipient.configName% ]]
5
14
  <b>%recipient.greet%</b>,<br/>
6
15
  <br/>
7
- Thanks for trying out Nitro! <br/>
16
+ Thanks for trying out %recipient.configName%! <br/>
8
17
  <br/>
9
18
  If you have any feature requests, feedback or questions, please don't hesitate to reach out to us at <a href="mailto:%recipient.replyToEmail%">%recipient.replyToEmail%</a>.<br/>
10
19
  <br/>
@@ -16,6 +25,6 @@
16
25
  </span><br/>
17
26
  <br/>
18
27
  Thanks,<br/>
19
- <b>The Nitro Team</b>
28
+ <b>The %recipient.configName% Team</b>
20
29
 
21
30
  {% endblock %}
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BC;AAED,6DAaC;AAED,4DAQC;AAED,iFAaC;AAED,0EAIC;AAED,gDAEC;AAED,0CAEC;AAED,qFAQC;AAED,uDAIC;AAED,iEAoBC;AAED;;;;EAgIC;AAED,wCASC;AAED,mDAYC;AAED,+DAyBC;AAED,iEAeC;AAED,kFA2BC;AAED,gEAIC;AAED,6CASC;AAED,qEAsEC;AAED,8CAGC;AAED,kDAQC;AAED;;;;IAOC;AAED;;;IAOC;AAED;;;;;GAKG;AACH,uCAJW,MAAM,iBACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;EAUC;AAED,sDAYC;AAED,uEAEC;AAED,kDAsBC;AAED,+DAaC;AAED,0DAEC;AAED,+CAEC;AAED,kDAEC;AAED,6CAIC;AAED,kEAOC;AAED,mDAEC;AAED,6CAiBC;AAED,iDAEC;AAED;;;;GAIG;AACH,mCAHW,OAAO,GACL,OAAO,CAKnB;AAED,gDAEC;AAED,iDAEC;AAED,0CAEC;AAED,yEAWC;AAED,mFAwCC;AAED;;;;;;;;;;;;;;;;;;;;;;;;EA0CC;AAED,uEAmBC;AAED,oDAKC;AAED,iDAOC;AAED;;;;;;;;GAQG;AACH,8FAyCC;AAED,kEAQC;AAED,8CAeC;AAED,+DAiCC;AAED,8CAcC;AAED,yFAiDC;AAED,oDAYC;AAED,4EAmBC;AAED,kDAUC;AAED,mFAiDC;AAED,oEAaC;AAED,8EAIC;AAED,0DAQC;AAED,uDAMC;AAED,mFAwBC;AAED;;;;EAyBC;AAED,8CAIC;AAED,uCAGC;AAED,0CAGC;AAxoCD,6BAAoC"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BC;AAED,6DAaC;AAED,4DAQC;AAED,iFAaC;AAED,0EAIC;AAED,gDAEC;AAED,0CAEC;AAED,qFAQC;AAED,uDAIC;AAED,iEAoBC;AAED;;;;EAgIC;AAED,wCASC;AAED,mDAYC;AAED,+DAyBC;AAED,iEAeC;AAED,kFA2BC;AAED,gEAIC;AAED,6CASC;AAED,qEAsEC;AAED,8CAGC;AAED,kDAQC;AAED;;;;IAOC;AAED;;;IAOC;AAED;;;;;GAKG;AACH,uCAJW,MAAM,iBACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;EAUC;AAED,sDAYC;AAED,uEAEC;AAED,kDAsBC;AAED,+DAaC;AAED,0DAEC;AAED,+CAEC;AAED,kDAEC;AAED,6CAIC;AAED,kEAOC;AAED,mDAEC;AAED,6CAiBC;AAED,iDAEC;AAED;;;;GAIG;AACH,mCAHW,OAAO,GACL,OAAO,CAKnB;AAED,gDAEC;AAED,iDAEC;AAED,0CAEC;AAED,yEAWC;AAED,mFAwCC;AAED;;;;;;;;;;;;;;;;;;;;;;;;EA0CC;AAED,uEAmBC;AAED,oDAKC;AAED,iDAOC;AAED;;;;;;;;GAQG;AACH,8FAyCC;AAED,kEAQC;AAED,8CAeC;AAED,+DAiCC;AAED,8CAcC;AAED,yFAkDC;AAED,oDAYC;AAED,4EAmBC;AAED,kDAUC;AAED,mFAiDC;AAED,oEAaC;AAED,8EAIC;AAED,0DAQC;AAED,uDAMC;AAED,mFAwBC;AAED;;;;EAyBC;AAED,8CAIC;AAED,uCAGC;AAED,0CAGC;AAzoCD,6BAAoC"}
package/util.js CHANGED
@@ -955,6 +955,7 @@ export async function request (route, data, event, isLoading) {
955
955
  return res.value.data
956
956
 
957
957
  } catch (errs) {
958
+ if (isLoading) isLoading[1](false)
958
959
  throw getResponseErrors(errs)
959
960
  }
960
961
  }