nitro-web 0.1.4 → 0.2.0
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/app.tsx +5 -5
- package/client/index.ts +1 -1
- package/client/store.ts +15 -5
- package/components/auth/auth.api.js +304 -117
- package/components/auth/inviteConfirm.tsx +57 -37
- package/components/auth/reset.tsx +9 -4
- package/components/auth/signin.tsx +12 -5
- package/components/auth/signup.tsx +6 -4
- package/package.json +1 -1
- package/server/email/index.js +34 -32
- package/server/index.js +1 -1
- package/server/models/company.js +3 -2
- package/server/models/user.js +2 -1
- package/server/router.js +6 -5
- package/types/components/auth/auth.api.d.ts +40 -11
- package/types/components/auth/auth.api.d.ts.map +1 -1
- package/types/server/email/index.d.ts +2 -0
- package/types/server/email/index.d.ts.map +1 -1
- package/types/server/index.d.ts +1 -1
- package/types/server/index.d.ts.map +1 -1
- package/types/server/models/company.d.ts +5 -1
- package/types/server/models/company.d.ts.map +1 -1
- package/types/server/models/user.d.ts +1 -1
- package/types/server/models/user.d.ts.map +1 -1
- package/types/util.d.ts +2 -2
- package/types/util.d.ts.map +1 -1
- package/types.ts +2 -1
- package/util.js +4 -3
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { Topbar, Field, FormError, Button, request, onChange, getResponseErrors,
|
|
2
|
-
import { Errors } from 'nitro-web/types'
|
|
1
|
+
import { Topbar, Field, FormError, Button, request, onChange, getResponseErrors, getSignoutStore, getInitialStore } from 'nitro-web'
|
|
2
|
+
import { Config, Errors } from 'nitro-web/types'
|
|
3
|
+
import { twMerge } from 'nitro-web/util'
|
|
3
4
|
import { Fragment, useEffect } from 'react'
|
|
4
5
|
|
|
5
6
|
type InviteConfirmProps = {
|
|
6
7
|
className?: string,
|
|
7
8
|
elements?: { Button?: typeof Button, Header?: React.ReactNode },
|
|
8
9
|
redirectTo?: string,
|
|
10
|
+
config: Pick<Config, 'getSignoutStore'>
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function InviteConfirm({ className, elements, redirectTo }: InviteConfirmProps) {
|
|
13
|
+
export function InviteConfirm({ className, elements, redirectTo, config }: InviteConfirmProps) {
|
|
12
14
|
const navigate = useNavigate()
|
|
13
15
|
const params = useParams()
|
|
14
|
-
const [
|
|
16
|
+
const [, setStore] = useTracked()
|
|
17
|
+
const getSignoutStoreFn = config.getSignoutStore || getSignoutStore
|
|
15
18
|
const [isLoading, setIsLoading] = useState(false)
|
|
16
|
-
const [
|
|
19
|
+
const [isExistingUser, setIsExistingUser] = useState<boolean | 'pending'>('pending')
|
|
20
|
+
const [isAccepted, setIsAccepted] = useState(false)
|
|
17
21
|
const [state, setState] = useState(() => ({
|
|
18
22
|
firstName: '',
|
|
19
23
|
lastName: '',
|
|
20
24
|
password: '',
|
|
21
25
|
password2: '',
|
|
22
|
-
|
|
26
|
+
email: '',
|
|
23
27
|
errors: [] as Errors,
|
|
24
28
|
}))
|
|
25
29
|
|
|
@@ -28,45 +32,56 @@ export function InviteConfirm({ className, elements, redirectTo }: InviteConfirm
|
|
|
28
32
|
Header: elements?.Header || null,
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
//
|
|
35
|
+
// Get invite details on mount
|
|
32
36
|
useEffect(() => {
|
|
33
|
-
|
|
37
|
+
preSubmit()
|
|
34
38
|
}, [])
|
|
35
39
|
|
|
40
|
+
async function preSubmit() {
|
|
41
|
+
try {
|
|
42
|
+
const result = await request(`get /api/invite-pre-confirm/${params.token}`)
|
|
43
|
+
setIsExistingUser(result.isExistingUser)
|
|
44
|
+
setState((s) => ({ ...s, email: result.email }))
|
|
45
|
+
if (result.isExistingUser) submit({ token: params.token })
|
|
46
|
+
} catch (e) {
|
|
47
|
+
setState((s) => ({ ...s, errors: getResponseErrors(e) }))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
async function submit(data: object, event?: React.FormEvent<HTMLFormElement>) {
|
|
37
52
|
try {
|
|
38
53
|
if (isLoading) return
|
|
39
|
-
const result = await request(
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
const result = await request(`post /api/invite-confirm/${params.token}`, data, event, setIsLoading, setState)
|
|
55
|
+
// Only update the store if the user was created AND refreshly signed in
|
|
56
|
+
if (result?.jwt) setStore((s) => ({ ...getSignoutStoreFn(s, getInitialStore()), ...result }))
|
|
57
|
+
setIsAccepted(true)
|
|
42
58
|
setTimeout(() => navigate(redirectTo || '/'), 5000)
|
|
43
59
|
} catch (e) {
|
|
44
|
-
showError(setStore, e)
|
|
45
60
|
setState((s) => ({ ...s, errors: getResponseErrors(e) }))
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
if (
|
|
64
|
+
if (isExistingUser || isAccepted) {
|
|
50
65
|
return (
|
|
51
|
-
<div className={className}>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
<span class="text-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
<div className={twMerge('min-h-[250px]', className)}>
|
|
67
|
+
{isAccepted ? (
|
|
68
|
+
<Fragment>
|
|
69
|
+
<div class="text-2xl font-bold mb-4">Your invite has been accepted.</div>
|
|
70
|
+
<p class="">You'll be redirected back to the <Link to="/">home page</Link> shortly...</p>
|
|
71
|
+
</Fragment>
|
|
72
|
+
) : isExistingUser === 'pending' && !state.errors.length ? (
|
|
73
|
+
<Fragment>
|
|
74
|
+
<div class="text-2xl font-bold mb-4">One moment please...</div>
|
|
75
|
+
<p class="">Verifying your token.</p>
|
|
76
|
+
</Fragment>
|
|
77
|
+
) : (
|
|
78
|
+
<Fragment>
|
|
79
|
+
<div class="text-2xl font-bold mb-4">Something went wrong.</div>
|
|
80
|
+
{state.errors.map((error, i) => {
|
|
81
|
+
return (<span key={i} class="text-red-500 bg-red-50 p-1 rounded-md">{error.detail} <br /></span>)
|
|
82
|
+
})}
|
|
83
|
+
</Fragment>
|
|
84
|
+
)}
|
|
70
85
|
</div>
|
|
71
86
|
)
|
|
72
87
|
}
|
|
@@ -74,19 +89,25 @@ export function InviteConfirm({ className, elements, redirectTo }: InviteConfirm
|
|
|
74
89
|
return (
|
|
75
90
|
<div className={className}>
|
|
76
91
|
{!!Elements.Header && Elements.Header}
|
|
77
|
-
<Topbar title={<Fragment>Accept
|
|
92
|
+
<Topbar title={<Fragment>Accept Invitation</Fragment>} />
|
|
78
93
|
|
|
79
94
|
<form onSubmit={(e) => submit(state, e)} class="mb-0">
|
|
80
95
|
<div class="grid grid-cols-2 gap-6">
|
|
81
96
|
<div>
|
|
82
97
|
<label for="firstName">First Name</label>
|
|
83
|
-
<Field name="firstName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your first name..."
|
|
98
|
+
<Field name="firstName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your first name..."
|
|
99
|
+
autoComplete="given-name" />
|
|
84
100
|
</div>
|
|
85
101
|
<div>
|
|
86
102
|
<label for="lastName">Last Name</label>
|
|
87
|
-
<Field name="lastName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your last name..."
|
|
103
|
+
<Field name="lastName" type="text" state={state} onChange={(e) => onChange(e, setState)} placeholder="Your last name..."
|
|
104
|
+
autoComplete="off" />
|
|
88
105
|
</div>
|
|
89
106
|
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<label for="email">Email Address</label>
|
|
109
|
+
<Field name="email" type="email" state={state} placeholder="Your email address..." disabled={true} />
|
|
110
|
+
</div>
|
|
90
111
|
<div>
|
|
91
112
|
<label for="password">Choose a Password</label>
|
|
92
113
|
<Field name="password" type="password" state={state} onChange={(e) => onChange(e, setState)} />
|
|
@@ -97,8 +118,7 @@ export function InviteConfirm({ className, elements, redirectTo }: InviteConfirm
|
|
|
97
118
|
</div>
|
|
98
119
|
|
|
99
120
|
<div class="mb-14">
|
|
100
|
-
|
|
101
|
-
<FormError state={state} className="pt-2" />
|
|
121
|
+
<FormError state={state} className="pt-2" fields={['firstName', 'lastName', 'password', 'password2']} />
|
|
102
122
|
</div>
|
|
103
123
|
|
|
104
124
|
<Elements.Button class="w-full" isLoading={isLoading} type="submit">Accept Invite & Create Account</Elements.Button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Topbar, Field, FormError, Button, request, onChange } from 'nitro-web'
|
|
2
|
-
import { Errors } from 'nitro-web/types'
|
|
1
|
+
import { Topbar, Field, FormError, Button, request, onChange, getSignoutStore, getInitialStore } from 'nitro-web'
|
|
2
|
+
import { Config, Errors } from 'nitro-web/types'
|
|
3
3
|
import { Fragment } from 'react'
|
|
4
4
|
|
|
5
5
|
type resetInstructionsProps = {
|
|
@@ -8,6 +8,10 @@ type resetInstructionsProps = {
|
|
|
8
8
|
redirectTo?: string,
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type resetPasswordProps = resetInstructionsProps & {
|
|
12
|
+
config: Pick<Config, 'getSignoutStore'>
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export function ResetInstructions({ className, elements, redirectTo }: resetInstructionsProps) {
|
|
12
16
|
const navigate = useNavigate()
|
|
13
17
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -52,9 +56,10 @@ export function ResetInstructions({ className, elements, redirectTo }: resetInst
|
|
|
52
56
|
)
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
export function ResetPassword({ className, elements, redirectTo }:
|
|
59
|
+
export function ResetPassword({ className, elements, redirectTo, config }: resetPasswordProps) {
|
|
56
60
|
const navigate = useNavigate()
|
|
57
61
|
const params = useParams()
|
|
62
|
+
const getSignoutStoreFn = config.getSignoutStore || getSignoutStore
|
|
58
63
|
const [isLoading, setIsLoading] = useState(false)
|
|
59
64
|
const [, setStore] = useTracked()
|
|
60
65
|
const [state, setState] = useState(() => ({
|
|
@@ -73,7 +78,7 @@ export function ResetPassword({ className, elements, redirectTo }: resetInstruct
|
|
|
73
78
|
try {
|
|
74
79
|
if (isLoading) return
|
|
75
80
|
const data = await request('post /api/reset-password', state, event, setIsLoading, setState)
|
|
76
|
-
setStore((s) => ({ ...s, ...data }))
|
|
81
|
+
setStore((s) => ({ ...getSignoutStoreFn(s, getInitialStore()), ...data }))
|
|
77
82
|
setTimeout(() => navigate(redirectTo || '/'), 10) // wait for setStore
|
|
78
83
|
} catch (e) {
|
|
79
84
|
return setState({ ...state, errors: e as Errors })
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Topbar, Field, Button, FormError, request, queryObject, injectedConfig, updateJwt, onChange,
|
|
3
|
+
getSignoutStore, getInitialStore,
|
|
4
|
+
} from 'nitro-web'
|
|
5
|
+
import { Config, Errors } from 'nitro-web/types'
|
|
3
6
|
import { Fragment } from 'react'
|
|
4
7
|
|
|
5
8
|
type signinProps = {
|
|
@@ -7,12 +10,14 @@ type signinProps = {
|
|
|
7
10
|
elements?: { Button?: typeof Button, Header?: React.ReactNode },
|
|
8
11
|
redirectTo?: string,
|
|
9
12
|
hideSignup?: boolean,
|
|
13
|
+
config: Pick<Config, 'getSignoutStore'>
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
export function Signin({ className, elements, redirectTo, hideSignup }: signinProps) {
|
|
16
|
+
export function Signin({ className, elements, redirectTo, hideSignup, config }: signinProps) {
|
|
13
17
|
const navigate = useNavigate()
|
|
14
18
|
const location = useLocation()
|
|
15
19
|
const isSignout = location.pathname == '/signout'
|
|
20
|
+
const getSignoutStoreFn = config.getSignoutStore || getSignoutStore
|
|
16
21
|
const [isLoading, setIsLoading] = useState(isSignout)
|
|
17
22
|
const [, setStore] = useTracked()
|
|
18
23
|
const [state, setState] = useState({
|
|
@@ -32,9 +37,11 @@ export function Signin({ className, elements, redirectTo, hideSignup }: signinPr
|
|
|
32
37
|
if (query.email) setState({ ...state, email: query.email as string })
|
|
33
38
|
}, [location.search])
|
|
34
39
|
|
|
40
|
+
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
if (isSignout) {
|
|
37
|
-
|
|
43
|
+
// Reset the user to the initialStoreData user
|
|
44
|
+
setStore((s) => getSignoutStoreFn(s, getInitialStore()))
|
|
38
45
|
// util.axios().get('/api/signout')
|
|
39
46
|
Promise.resolve()
|
|
40
47
|
.then(() => setIsLoading(false))
|
|
@@ -50,7 +57,7 @@ export function Signin({ className, elements, redirectTo, hideSignup }: signinPr
|
|
|
50
57
|
const data = await request('post /api/signin', state, e, setIsLoading, setState)
|
|
51
58
|
// Keep it loading until we navigate
|
|
52
59
|
setIsLoading(true)
|
|
53
|
-
setStore((s) => ({ ...s, ...data }))
|
|
60
|
+
setStore((s) => ({ ...getSignoutStoreFn(s, getInitialStore()), ...data }))
|
|
54
61
|
setTimeout(() => { // wait for setStore
|
|
55
62
|
if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
|
|
56
63
|
else navigate(redirectTo || '/')
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { Button, Field, FormError, Topbar, request, injectedConfig, onChange } from 'nitro-web'
|
|
2
|
-
import { Errors } from 'nitro-web/types'
|
|
1
|
+
import { Button, Field, FormError, Topbar, request, injectedConfig, onChange, getSignoutStore, getInitialStore } from 'nitro-web'
|
|
2
|
+
import { Config, Errors } from 'nitro-web/types'
|
|
3
3
|
import { Fragment } from 'react'
|
|
4
4
|
|
|
5
5
|
type signupProps = {
|
|
6
6
|
className?: string,
|
|
7
7
|
elements?: { Button?: typeof Button, Header?: React.ReactNode },
|
|
8
8
|
redirectTo?: string,
|
|
9
|
+
config: Pick<Config, 'getSignoutStore'>
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export function Signup({ className, elements, redirectTo }: signupProps) {
|
|
12
|
+
export function Signup({ className, elements, redirectTo, config }: signupProps) {
|
|
12
13
|
const navigate = useNavigate()
|
|
13
14
|
const [isLoading, setIsLoading] = useState(false)
|
|
14
15
|
const [, setStore] = useTracked()
|
|
16
|
+
const getSignoutStoreFn = config.getSignoutStore || getSignoutStore
|
|
15
17
|
const [state, setState] = useState({
|
|
16
18
|
email: injectedConfig.env === 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
17
19
|
name: injectedConfig.env === 'development' ? 'Bruce Wayne' : '',
|
|
@@ -29,7 +31,7 @@ export function Signup({ className, elements, redirectTo }: signupProps) {
|
|
|
29
31
|
try {
|
|
30
32
|
if (isLoading) return
|
|
31
33
|
const data = await request('post /api/signup', state, e, setIsLoading, setState)
|
|
32
|
-
setStore((
|
|
34
|
+
setStore((s) => ({ ...getSignoutStoreFn(s, getInitialStore()), ...data }))
|
|
33
35
|
setTimeout(() => navigate(redirectTo || '/'), 10) // wait for setStore
|
|
34
36
|
} catch (e) {
|
|
35
37
|
setState((prev) => ({ ...prev, errors: e as Errors }))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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/email/index.js
CHANGED
|
@@ -13,6 +13,10 @@ let templates = {}
|
|
|
13
13
|
let nodemailerMailgun = undefined
|
|
14
14
|
const _dirname = dirname(fileURLToPath(import.meta.url)) + '/'
|
|
15
15
|
|
|
16
|
+
export const requiredEmailConfigKeys = ['baseUrl', 'emailFrom', 'name', 'env']
|
|
17
|
+
export const optionalEmailConfigKeys = ['emailReplyTo', 'emailTestMode', 'mailgunDomain', 'mailgunKey']
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* Sends an email using a predefined template, with optional data/or recipientVariables
|
|
18
22
|
* @typedef {{ baseUrl?: string, emailFrom?: string, mailgunDomain?: string, mailgunKey?: string, name?: string }} Config
|
|
@@ -44,24 +48,19 @@ export async function sendEmail({
|
|
|
44
48
|
skipCssInline,
|
|
45
49
|
test,
|
|
46
50
|
}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error(
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new Error('sendEmail: `config.mailgunKey` or `config.mailgunDomain` is missing')
|
|
55
|
-
} else if (!config.name) {
|
|
56
|
-
throw new Error('sendEmail: `config.name` is missing')
|
|
57
|
-
} else if (!template) {
|
|
58
|
-
throw new Error('sendEmail: `template` missing')
|
|
59
|
-
} else if (!to) {
|
|
60
|
-
throw new Error('sendEmail: `to` missing')
|
|
51
|
+
const isTest = config.emailTestMode || test
|
|
52
|
+
if (!config) throw new Error('sendEmail: `config` missing')
|
|
53
|
+
for (const key of requiredEmailConfigKeys) {
|
|
54
|
+
if (!config[key]) throw new Error(`sendEmail: config.${key} is missing`)
|
|
55
|
+
}
|
|
56
|
+
if (!isTest && (!config.mailgunKey || !config.mailgunDomain)) {
|
|
57
|
+
throw new Error('sendEmail: config.mailgunKey and config.mailgunDomain are required')
|
|
61
58
|
}
|
|
59
|
+
if (!template) throw new Error('sendEmail: `template` missing')
|
|
60
|
+
if (!to) throw new Error('sendEmail: `to` missing')
|
|
62
61
|
|
|
63
62
|
// Setup nodemailer once
|
|
64
|
-
if (!nodemailerMailgun && !
|
|
63
|
+
if (!nodemailerMailgun && !isTest) {
|
|
65
64
|
nodemailerMailgun = nodemailer.createTransport(
|
|
66
65
|
mailgun({ auth: { api_key: config.mailgunKey, domain: config.mailgunDomain }})
|
|
67
66
|
)
|
|
@@ -95,13 +94,13 @@ export async function sendEmail({
|
|
|
95
94
|
bcc: bcc,
|
|
96
95
|
emailTemplateDir: getDirectories(path, config.pwd).emailTemplateDir,
|
|
97
96
|
from: from,
|
|
98
|
-
isDev: config.
|
|
97
|
+
isDev: config.env === 'development',
|
|
99
98
|
recipientVariables: recipientVariables,
|
|
100
99
|
replyTo: replyTo,
|
|
101
100
|
skipCssInline: skipCssInline,
|
|
102
101
|
subject: subject,
|
|
103
102
|
template: template,
|
|
104
|
-
test:
|
|
103
|
+
test: isTest,
|
|
105
104
|
to: to,
|
|
106
105
|
url: config.baseUrl,
|
|
107
106
|
}
|
|
@@ -187,24 +186,27 @@ function processTemplate(settings, html) {
|
|
|
187
186
|
async function sendWithMailgun(settings, html) {
|
|
188
187
|
// Supports batch sending via recipientVariables, limit 1000 emails
|
|
189
188
|
// https://documentation.mailgun.com/en/latest/user_manual.html?highlight=batch%20sending#batch-sending
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
const processedhtml = await processTemplate(settings, html)
|
|
190
|
+
const mailgunOpts = {
|
|
191
|
+
...(settings.bcc && !settings.isDev? { bcc: settings.bcc } : {}),
|
|
192
|
+
from: settings.from,
|
|
193
|
+
html: processedhtml,
|
|
194
|
+
'h:Reply-To': settings.replyTo,
|
|
195
|
+
subject: settings.subject,
|
|
196
|
+
to: settings.to,
|
|
197
|
+
...(!settings.recipientVariables? {} : {
|
|
198
|
+
'recipient-variables': typeof settings.recipientVariables == 'string'
|
|
199
|
+
? settings.recipientVariables
|
|
200
|
+
: JSON.stringify(settings.recipientVariables),
|
|
201
|
+
}),
|
|
202
|
+
}
|
|
203
|
+
if (settings.test && settings.isDev) {
|
|
204
|
+
console.info('Test mode: sendEmail mailgunOpts', { ...mailgunOpts, html: null, 'recipient-variables': settings.recipientVariables })
|
|
205
|
+
}
|
|
192
206
|
if (settings.test) return processedhtml
|
|
193
207
|
|
|
194
208
|
return new Promise((resolve, reject) => {
|
|
195
|
-
nodemailerMailgun.sendMail({
|
|
196
|
-
...(settings.bcc && !settings.isDev? { bcc: settings.bcc } : {}),
|
|
197
|
-
from: settings.from,
|
|
198
|
-
html: processedhtml,
|
|
199
|
-
'h:Reply-To': settings.replyTo,
|
|
200
|
-
subject: settings.subject,
|
|
201
|
-
to: settings.to,
|
|
202
|
-
...(!settings.recipientVariables? {} : {
|
|
203
|
-
'recipient-variables': typeof settings.recipientVariables == 'string'
|
|
204
|
-
? settings.recipientVariables
|
|
205
|
-
: JSON.stringify(settings.recipientVariables),
|
|
206
|
-
}),
|
|
207
|
-
}, function(err, info) {
|
|
209
|
+
nodemailerMailgun.sendMail(mailgunOpts, function(err, info) {
|
|
208
210
|
if (err) {
|
|
209
211
|
console.error('SendEmail mailgun error')
|
|
210
212
|
reject(err)
|
package/server/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { userModel, companyModel, setupDefaultModels }
|
|
|
24
24
|
export { setupRouter, middleware, isValidUserOrRespond, isAdminUser } from './router.js'
|
|
25
25
|
|
|
26
26
|
// Export email utility
|
|
27
|
-
export { sendEmail } from './email/index.js'
|
|
27
|
+
export { sendEmail, requiredEmailConfigKeys, optionalEmailConfigKeys } from './email/index.js'
|
|
28
28
|
|
|
29
29
|
// Export API controllers
|
|
30
30
|
export * from '../components/auth/auth.api.js'
|
package/server/models/company.js
CHANGED
|
@@ -21,6 +21,7 @@ export default {
|
|
|
21
21
|
}],
|
|
22
22
|
invites: [{
|
|
23
23
|
email: { type: 'email', required: true },
|
|
24
|
+
firstName: { type: 'string', required: true },
|
|
24
25
|
role: { type: 'string', enum: ['owner', 'manager'], required: true },
|
|
25
26
|
inviteToken: { type: 'string', required: true },
|
|
26
27
|
}],
|
|
@@ -57,8 +58,8 @@ export default {
|
|
|
57
58
|
publicData: function(models) {
|
|
58
59
|
return models
|
|
59
60
|
},
|
|
60
|
-
|
|
61
|
-
//
|
|
61
|
+
authPopulate: function() {
|
|
62
|
+
// Special method called by auth.api.js on authentication.
|
|
62
63
|
return [
|
|
63
64
|
{
|
|
64
65
|
as: 'usersExpanded',
|
package/server/models/user.js
CHANGED
package/server/router.js
CHANGED
|
@@ -387,7 +387,7 @@ export const middleware = {
|
|
|
387
387
|
else next()
|
|
388
388
|
},
|
|
389
389
|
isUser: (req, res, next) => {
|
|
390
|
-
if (!isValidUserOrRespond(req, res)) return
|
|
390
|
+
if (!isValidUserOrRespond(req, res)) return
|
|
391
391
|
else next()
|
|
392
392
|
},
|
|
393
393
|
isParamUser: (req, res, next) => {
|
|
@@ -401,11 +401,11 @@ export const middleware = {
|
|
|
401
401
|
else next()
|
|
402
402
|
},
|
|
403
403
|
isCompanyUser: (req, res, next) => {
|
|
404
|
-
if (!isValidParamCompanyUserOrRespond(req)) return
|
|
404
|
+
if (!isValidParamCompanyUserOrRespond(req, res)) return
|
|
405
405
|
else next()
|
|
406
406
|
},
|
|
407
407
|
isCompanyOwner: (req, res, next) => {
|
|
408
|
-
if (!isValidParamCompanyUserOrRespond(req, true)) return
|
|
408
|
+
if (!isValidParamCompanyUserOrRespond(req, res, true)) return
|
|
409
409
|
else next()
|
|
410
410
|
},
|
|
411
411
|
}
|
|
@@ -433,7 +433,8 @@ function isValidParamCompanyUserOrRespond(req, res, checkIsOwner = false) {
|
|
|
433
433
|
const company = _company || req.user?.companies?.find((o) => o._id.toString() == req.params.cid)
|
|
434
434
|
const isCompanyOwner = company?.users?.find((o) => o._id.toString() == req.user?._id?.toString() && o.role === 'owner')
|
|
435
435
|
if (!isValidUserOrRespond(req, res)) return
|
|
436
|
-
else if (!isAdminUser(req) &&
|
|
437
|
-
else if (!isAdminUser(req) &&
|
|
436
|
+
else if (!isAdminUser(req) && configLocal.isNotMultiTenant) res.unauthorized('Only admins can make this request.')
|
|
437
|
+
else if (!isAdminUser(req) && !company) res.unauthorized('Only company users can make this request.')
|
|
438
|
+
else if (!isAdminUser(req) && checkIsOwner && !isCompanyOwner) res.unauthorized('Only company owners can make this request.')
|
|
438
439
|
else return true
|
|
439
440
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export function resetInstructions(req: any, res: any): Promise<void>;
|
|
2
|
-
export function inviteInstructions(req: any, res: any): Promise<
|
|
2
|
+
export function inviteInstructions(req: any, res: any): Promise<any>;
|
|
3
|
+
export function resendInstructions(req: any, res: any): Promise<any>;
|
|
3
4
|
export function resetConfirm(req: any, res: any): Promise<void>;
|
|
4
5
|
export function inviteConfirm(req: any, res: any): Promise<void>;
|
|
6
|
+
export function invitePreConfirm(req: any, res: any): Promise<void>;
|
|
7
|
+
export function updateMemberRole(req: any, res: any): Promise<void>;
|
|
8
|
+
export function removeMember(req: any, res: any): Promise<void>;
|
|
5
9
|
export function userFindFromProvider(query: any, passwordToCheck: any, ...args: any[]): Promise<any>;
|
|
6
10
|
export function userSigninGetStore(user: any, isDesktop: any): Promise<{
|
|
7
11
|
jwt: string;
|
|
@@ -24,10 +28,11 @@ export function userCreate({ password, password2, company, ...userDataProp }: {
|
|
|
24
28
|
password?: string;
|
|
25
29
|
password2?: string;
|
|
26
30
|
company?: string;
|
|
27
|
-
}, baseUrl?: string, skipSendEmail?: boolean): Promise<object>;
|
|
31
|
+
}, baseUrl?: string, invite: any, skipSendEmail?: boolean): Promise<object>;
|
|
28
32
|
export function passwordValidate(password: string, password2: any): Promise<void>;
|
|
29
33
|
export function tokenCreate(modelName: any, id: any): Promise<any>;
|
|
30
34
|
export function tokenParse(token: any, modelName: any, maxAgeMs?: number): any;
|
|
35
|
+
export function addUserToCompany(companyId: any, userId: any, role: any, token: any, justValidate: any): Promise<any>;
|
|
31
36
|
export function tokenConfirmForReset(req: any): Promise<{
|
|
32
37
|
jwt: string;
|
|
33
38
|
user: any;
|
|
@@ -39,24 +44,36 @@ export function tokenConfirmForSingleTenant(req: any, isReset: any): Promise<{
|
|
|
39
44
|
export function tokenConfirmForMultiTenant(req: any): Promise<{
|
|
40
45
|
jwt: string;
|
|
41
46
|
user: any;
|
|
47
|
+
} | {
|
|
48
|
+
isExistingUser: boolean;
|
|
49
|
+
email: any;
|
|
50
|
+
} | {
|
|
51
|
+
isExistingUser?: undefined;
|
|
52
|
+
email?: undefined;
|
|
42
53
|
}>;
|
|
43
54
|
/**
|
|
44
55
|
* Creates and sends a reset or invite token to a user or company
|
|
45
56
|
* @param {object} options
|
|
46
|
-
* @param {'reset' | 'invite' | 'companyInvite'} options.type - token type
|
|
47
|
-
* @param {string} options.
|
|
48
|
-
* @param {
|
|
49
|
-
*
|
|
57
|
+
* @param {'reset' | 'invite' | 'companyInvite'} options.type - token type
|
|
58
|
+
* @param {string} options.id - user or company id
|
|
59
|
+
* @param {{
|
|
60
|
+
* email: string,
|
|
61
|
+
* firstName: string,
|
|
62
|
+
* [key: string]: any, // other fields to include in the invite row
|
|
63
|
+
* }} options.payload
|
|
50
64
|
* @param {function} [options.beforeUpdate] - runs before updating the model with the token, return null to skip update
|
|
51
65
|
* @param {function} [options.beforeSendEmail] - runs before sending the email, receives (options, token)
|
|
52
66
|
* @param {string} [options.baseUrl] - baseUrl to use for the email
|
|
53
67
|
* @returns {Promise<{token: string, mailgunPromise: Promise<unknown>}>}
|
|
54
68
|
*/
|
|
55
|
-
export function tokenSend({ type,
|
|
69
|
+
export function tokenSend({ type, id, payload, beforeUpdate, beforeSendEmail, baseUrl, isResend }: {
|
|
56
70
|
type: "reset" | "invite" | "companyInvite";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
id: string;
|
|
72
|
+
payload: {
|
|
73
|
+
email: string;
|
|
74
|
+
firstName: string;
|
|
75
|
+
[key: string]: any;
|
|
76
|
+
};
|
|
60
77
|
beforeUpdate?: Function;
|
|
61
78
|
beforeSendEmail?: Function;
|
|
62
79
|
baseUrl?: string;
|
|
@@ -64,11 +81,14 @@ export function tokenSend({ type, _id, email, firstName, beforeUpdate, beforeSen
|
|
|
64
81
|
token: string;
|
|
65
82
|
mailgunPromise: Promise<unknown>;
|
|
66
83
|
}>;
|
|
84
|
+
export function getBaseUrl(req: any): any;
|
|
67
85
|
export function resolveBaseUrl(reqUrl: any, cfgUrl: any): any;
|
|
86
|
+
export function ensureNotLastOwner(companyUsers: any, idNowNonOwner: any): void;
|
|
68
87
|
export namespace auth {
|
|
69
88
|
export { userFindFromProvider };
|
|
70
89
|
export { userSigninGetStore };
|
|
71
90
|
export { getStore };
|
|
91
|
+
export { addUserToCompany };
|
|
72
92
|
export { userCreate };
|
|
73
93
|
export { passwordValidate };
|
|
74
94
|
export { tokenCreate };
|
|
@@ -77,6 +97,11 @@ export namespace auth {
|
|
|
77
97
|
export { tokenConfirmForReset };
|
|
78
98
|
export { tokenConfirmForSingleTenant };
|
|
79
99
|
export { tokenConfirmForMultiTenant };
|
|
100
|
+
export { getBaseUrl };
|
|
101
|
+
export { invitePreConfirm };
|
|
102
|
+
export { inviteConfirm };
|
|
103
|
+
export { updateMemberRole };
|
|
104
|
+
export { removeMember };
|
|
80
105
|
}
|
|
81
106
|
export const routes: {
|
|
82
107
|
'get /api/store': (typeof store)[];
|
|
@@ -86,8 +111,12 @@ export const routes: {
|
|
|
86
111
|
'post /api/reset-instructions': (typeof resetInstructions)[];
|
|
87
112
|
'post /api/reset-confirm': (typeof resetConfirm)[];
|
|
88
113
|
'post /api/invite-instructions': (typeof inviteInstructions)[];
|
|
89
|
-
'post /api/
|
|
114
|
+
'post /api/resend-instructions': (typeof resendInstructions)[];
|
|
115
|
+
'get /api/invite-pre-confirm/:token': (typeof invitePreConfirm)[];
|
|
116
|
+
'post /api/invite-confirm/:token': (typeof inviteConfirm)[];
|
|
90
117
|
'delete /api/account/:uid': (typeof remove)[];
|
|
118
|
+
'put /api/company/:cid/member-role/:uidOrEmail': (string | typeof updateMemberRole)[];
|
|
119
|
+
'delete /api/company/:cid/member/:uidOrEmail': (string | typeof removeMember)[];
|
|
91
120
|
setup: typeof setup;
|
|
92
121
|
};
|
|
93
122
|
declare function store(req: any, res: any): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"AAoLA,qEAUC;AAED,qEAwEC;AAED,qEAGC;AAED,gEAMC;AAED,iEAgBC;AAED,oEAGC;AAED,oEA8BC;AAED,gEAgCC;AAID,qGAkDC;AAED;;;GAOC;AAED;;GAKC;AAID;;;;;;;;;GASG;AACH,8EAPG;IAA0B,QAAQ,GAA1B,MAAM;IACY,SAAS,GAA3B,MAAM;IACY,OAAO,GAAzB,MAAM;CACd,YAAQ,MAAM,+BACN,OAAO,GACL,OAAO,CAAC,MAAM,CAAC,CAqE3B;AAED,kFAiBC;AAED,mEAOC;AAED,+EAWC;AAED,sHAiBC;AAED;;;GAEC;AAED;;;GA0BC;AAED;;;;;;;;;GAmCC;AAED;;;;;;;;;;;;;;GAcG;AACH,mGAZG;IAAsD,IAAI,EAAlD,OAAO,GAAG,QAAQ,GAAG,eAAe;IACpB,EAAE,EAAlB,MAAM;IAKH,OAAO,EAJV;QACN,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QACtB,CAAK,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB;IAC0B,YAAY;IACZ,eAAe;IACjB,OAAO,GAAxB,MAAM;CACd,GAAU,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAC,CAAC,CAmEtE;AAED,0CAEC;AAED,8DAmBC;AAED,gFAKC;;;;;;;;;;;;;;;;;;;;AA/tBD;;;;;;;;;;;;;;;EAiBC;AA0ED,0DAEC;AAgCD,mDAEC;AAtBD,iDAkBC;AA5BD,2DAQC;AA0BD,2DAwBC;AAtID,0EAsEC"}
|
|
@@ -29,6 +29,8 @@ export function sendEmail({ template, to, config, bcc, data, from, replyTo, reci
|
|
|
29
29
|
skipCssInline?: boolean;
|
|
30
30
|
test?: boolean;
|
|
31
31
|
}): Promise<[any, any]>;
|
|
32
|
+
export const requiredEmailConfigKeys: string[];
|
|
33
|
+
export const optionalEmailConfigKeys: string[];
|
|
32
34
|
/**
|
|
33
35
|
* Sends an email using a predefined template, with optional data/or recipientVariables
|
|
34
36
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../server/email/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../server/email/index.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;GAiBG;AACH,iIAbG;IAAqB,QAAQ,EAArB,MAAM;IACO,EAAE,EAAf,MAAM;IACO,MAAM,EAAnB,MAAM;IACQ,GAAG,GAAjB,MAAM;IACQ,IAAI,GAAlB,MAAM;IACQ,IAAI,GAAlB,MAAM;IACQ,OAAO,GAArB,MAAM;IACQ,kBAAkB,GAAhC,MAAM;IACQ,OAAO,GAArB,MAAM;IACS,aAAa,GAA5B,OAAO;IACQ,IAAI,GAAnB,OAAO;CACf,GAAU,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CA4E/B;AAhGD,+CAA8E;AAC9E,+CAAuG;;;;qBAK1F;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE"}
|
package/types/server/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ export * from "../util.js";
|
|
|
2
2
|
export * from "../components/auth/auth.api.js";
|
|
3
3
|
export * from "../components/billing/stripe.api.js";
|
|
4
4
|
export * as util from "../util.js";
|
|
5
|
-
export { sendEmail } from "./email/index.js";
|
|
6
5
|
export { routes as authRoutes } from "../components/auth/auth.api.js";
|
|
7
6
|
export { routes as stripeRoutes } from "../components/billing/stripe.api.js";
|
|
8
7
|
/**
|
|
@@ -23,4 +22,5 @@ export function setupDefaultModels(db: any): Promise<void>;
|
|
|
23
22
|
export { userModel, companyModel };
|
|
24
23
|
export { currencies, countries } from "./constants.js";
|
|
25
24
|
export { setupRouter, middleware, isValidUserOrRespond, isAdminUser } from "./router.js";
|
|
25
|
+
export { sendEmail, requiredEmailConfigKeys, optionalEmailConfigKeys } from "./email/index.js";
|
|
26
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.js"],"names":[],"mappings":";;;;;;;;;+BAOa,OAAO,aAAa,EAAE,gBAAgB;;;;sBACtC,OAAO,gBAAgB,EAAE,OAAO;;;;uBAChC,OAAO,gBAAgB,EAAE,QAAQ;sBAIxB,kBAAkB;yBACf,qBAAqB;AAC9C,2DAIC"}
|
|
@@ -112,6 +112,10 @@ declare namespace _default {
|
|
|
112
112
|
type: string;
|
|
113
113
|
required: boolean;
|
|
114
114
|
};
|
|
115
|
+
firstName: {
|
|
116
|
+
type: string;
|
|
117
|
+
required: boolean;
|
|
118
|
+
};
|
|
115
119
|
role: {
|
|
116
120
|
type: string;
|
|
117
121
|
enum: string[];
|
|
@@ -128,7 +132,7 @@ declare namespace _default {
|
|
|
128
132
|
let afterFind: ((data: any) => Promise<void>)[];
|
|
129
133
|
namespace methods {
|
|
130
134
|
function publicData(models: any): any;
|
|
131
|
-
function
|
|
135
|
+
function authPopulate(): {
|
|
132
136
|
as: string;
|
|
133
137
|
from: string;
|
|
134
138
|
let: {
|