nitro-web 0.0.26 → 0.0.28
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 +36 -41
- package/client/globals.ts +0 -5
- package/client/index.ts +8 -3
- package/client/store.ts +35 -21
- package/components/auth/auth.api.js +63 -126
- package/components/auth/reset.tsx +6 -6
- package/components/auth/signin.tsx +14 -12
- package/components/auth/signup.tsx +11 -11
- package/components/partials/element/button.tsx +37 -32
- package/components/partials/element/sidebar.tsx +7 -8
- package/components/partials/styleguide.tsx +9 -7
- package/components/settings/settings-account.tsx +3 -3
- package/components/settings/settings-business.tsx +2 -2
- package/components/settings/settings-team--member.tsx +1 -1
- package/components/settings/settings-team.tsx +1 -1
- package/package.json +3 -3
- package/types/required-globals.d.ts +0 -1
- package/types/util.d.ts +1 -1
- package/types.ts +13 -8
- package/util.js +4 -4
package/client/app.tsx
CHANGED
|
@@ -2,10 +2,14 @@ import { createBrowserRouter, createHashRouter, redirect, useParams, RouterProvi
|
|
|
2
2
|
import { Fragment, ReactNode } from 'react'
|
|
3
3
|
import ReactDOM from 'react-dom/client'
|
|
4
4
|
import { AxiosRequestConfig } from '@hokify/axios'
|
|
5
|
-
import { beforeCreate, Provider, exposedData } from './store'
|
|
6
5
|
import { axios, camelCase, pick, toArray, setTimeoutPromise } from 'nitro-web/util'
|
|
6
|
+
import { injectedConfig, preloadedStoreData, exposedStoreData } from './index'
|
|
7
7
|
import { Config, Store } from 'nitro-web/types'
|
|
8
8
|
|
|
9
|
+
type StoreContainer = {
|
|
10
|
+
Provider: React.FC<{ children: ReactNode }>
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
type LayoutProps = {
|
|
10
14
|
config: Config;
|
|
11
15
|
}
|
|
@@ -13,7 +17,7 @@ type LayoutProps = {
|
|
|
13
17
|
type Settings = {
|
|
14
18
|
afterApp?: () => void
|
|
15
19
|
beforeApp: (config: Config) => Promise<object>
|
|
16
|
-
beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
|
|
20
|
+
// beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
|
|
17
21
|
isStatic?: boolean
|
|
18
22
|
layouts: React.FC<LayoutProps>[]
|
|
19
23
|
middleware: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
|
|
@@ -22,7 +26,7 @@ type Settings = {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
type Route = {
|
|
25
|
-
component: React.FC<{ route?: Route; params?: object; location?: object }>
|
|
29
|
+
component: React.FC<{ route?: Route; params?: object; location?: object; config?: Config }>
|
|
26
30
|
meta?: { title?: string }
|
|
27
31
|
middleware: string[]
|
|
28
32
|
name: string
|
|
@@ -30,26 +34,39 @@ type Route = {
|
|
|
30
34
|
redirect?: string
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
export async function setupApp(config: Config, layouts: React.FC<LayoutProps>[]) {
|
|
37
|
+
export async function setupApp(config: Config, storeContainer: StoreContainer, layouts: React.FC<LayoutProps>[]) {
|
|
38
|
+
if (!layouts) throw new Error('layouts are required')
|
|
34
39
|
// Fetch state and init app
|
|
35
40
|
const settings: Settings = {
|
|
36
41
|
beforeApp: config.beforeApp || beforeApp,
|
|
37
|
-
beforeStoreUpdate: config.beforeStoreUpdate || beforeStoreUpdate,
|
|
38
42
|
isStatic: config.isStatic,
|
|
39
43
|
layouts: layouts,
|
|
40
44
|
middleware: Object.assign(defaultMiddleware, config.middleware || {}),
|
|
41
45
|
name: config.name,
|
|
42
46
|
titleSeparator: config.titleSeparator,
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
|
|
49
|
+
// Setup the jwt token
|
|
50
|
+
updateJwt(localStorage.getItem(injectedConfig.jwtName))
|
|
51
|
+
|
|
52
|
+
// Fetch the store/state
|
|
53
|
+
const data = (await settings.beforeApp(config)) || {}
|
|
54
|
+
// Make the store data available to the store
|
|
55
|
+
Object.assign(preloadedStoreData, data)
|
|
47
56
|
|
|
48
57
|
const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement)
|
|
49
|
-
root.render(<App settings={settings} config={config} />)
|
|
58
|
+
root.render(<App settings={settings} config={config} storeContainer={storeContainer} />)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function updateJwt(token?: string | null) {
|
|
62
|
+
// Update the jwt token in local storage and axios headers
|
|
63
|
+
const key = injectedConfig.jwtName
|
|
64
|
+
localStorage.setItem(key, token || '')
|
|
65
|
+
if (token) axios().defaults.headers.Authorization = `Bearer ${token}`
|
|
66
|
+
else delete axios().defaults.headers.Authorization
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
function App({ settings, config }: { settings: Settings, config: Config }): ReactNode {
|
|
69
|
+
function App({ settings, config, storeContainer }: { settings: Settings, config: Config, storeContainer: StoreContainer }): ReactNode {
|
|
53
70
|
// const themeNormalised = theme
|
|
54
71
|
const router = getRouter({ settings, config })
|
|
55
72
|
// const theme = pick(themeNormalised, []) // e.g. 'topPanelHeight'
|
|
@@ -76,12 +93,12 @@ function App({ settings, config }: { settings: Settings, config: Config }): Reac
|
|
|
76
93
|
}, [!!router])
|
|
77
94
|
|
|
78
95
|
return (
|
|
79
|
-
<Provider>
|
|
96
|
+
<storeContainer.Provider>
|
|
80
97
|
{/* <ThemeProvider theme={themeNormalised}> */}
|
|
81
98
|
{ router && <RouterProvider router={router} /> }
|
|
82
99
|
<AfterApp settings={settings} />
|
|
83
100
|
{/* </ThemeProvider> */}
|
|
84
|
-
</Provider>
|
|
101
|
+
</storeContainer.Provider>
|
|
85
102
|
)
|
|
86
103
|
}
|
|
87
104
|
|
|
@@ -182,14 +199,14 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
182
199
|
children: layout.map((route) => {
|
|
183
200
|
return {
|
|
184
201
|
element: (
|
|
185
|
-
<RouteComponent route={route} />
|
|
202
|
+
<RouteComponent route={route} config={config} />
|
|
186
203
|
),
|
|
187
204
|
path: route.path,
|
|
188
205
|
loader: async () => { // request
|
|
189
|
-
// wait for container/
|
|
206
|
+
// wait for container/exposedStoreData to be setup
|
|
190
207
|
if (!nonce) nonce = true && await setTimeoutPromise(() => {}, 0) // eslint-disable-line
|
|
191
208
|
for (const key of route.middleware) {
|
|
192
|
-
const error = settings.middleware[key](route,
|
|
209
|
+
const error = settings.middleware[key](route, exposedStoreData || {})
|
|
193
210
|
if (error && error.redirect) {
|
|
194
211
|
return redirect(error.redirect)
|
|
195
212
|
}
|
|
@@ -230,13 +247,13 @@ function RestoreScroll() {
|
|
|
230
247
|
return (null)
|
|
231
248
|
}
|
|
232
249
|
|
|
233
|
-
function RouteComponent({ route }: { route: Route }) {
|
|
250
|
+
function RouteComponent({ route, config }: { route: Route, config: Config }) {
|
|
234
251
|
const Component = route.component
|
|
235
252
|
const params = useParams()
|
|
236
253
|
const location = useLocation()
|
|
237
254
|
document.title = route.meta?.title || ''
|
|
238
255
|
return (
|
|
239
|
-
<Component route={route} params={params} location={location} />
|
|
256
|
+
<Component route={route} params={params} location={location} config={config} />
|
|
240
257
|
)
|
|
241
258
|
}
|
|
242
259
|
|
|
@@ -261,7 +278,7 @@ async function beforeApp(config: Config) {
|
|
|
261
278
|
// sharedStoreCache = window.prehot.sharedStoreCache
|
|
262
279
|
// delete window.prehot
|
|
263
280
|
// }
|
|
264
|
-
if (!
|
|
281
|
+
if (!config.isStatic) {
|
|
265
282
|
stateData = (await axios().get('/api/state', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
|
|
266
283
|
apiAvailable = true
|
|
267
284
|
}
|
|
@@ -269,29 +286,7 @@ async function beforeApp(config: Config) {
|
|
|
269
286
|
console.error('We had trouble connecting to the API, please refresh')
|
|
270
287
|
console.log(err)
|
|
271
288
|
}
|
|
272
|
-
return { ...
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function beforeStoreUpdate(prevStore: Store | null, newData: Store) {
|
|
276
|
-
/**
|
|
277
|
-
* Get store object (called on signup/signin/signout/state)
|
|
278
|
-
* @param {object} store - existing store
|
|
279
|
-
* @param {object} <newStoreData> - pass to override store with /login or /state request data
|
|
280
|
-
* @return {object} store
|
|
281
|
-
*/
|
|
282
|
-
if (!newData) return newData
|
|
283
|
-
const store = {
|
|
284
|
-
...(prevStore || {
|
|
285
|
-
message: undefined,
|
|
286
|
-
user: undefined, // defined if user is signed in
|
|
287
|
-
}),
|
|
288
|
-
...(newData || {}),
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Used to verify if the current cookie belongs to this user
|
|
292
|
-
// E.g. signout > signin (to a different user on another tab)
|
|
293
|
-
axios().defaults.headers.userid = store?.user?._id
|
|
294
|
-
return store
|
|
289
|
+
return { ...stateData, apiAvailable }
|
|
295
290
|
}
|
|
296
291
|
|
|
297
292
|
const defaultMiddleware = {
|
package/client/globals.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
3
3
|
import { onChange } from 'nitro-web'
|
|
4
|
-
import { useTracked } from './store'
|
|
5
4
|
|
|
6
5
|
declare global {
|
|
7
6
|
// Common application globals
|
|
8
7
|
const onChange: typeof import('nitro-web').onChange
|
|
9
|
-
/** Global shared store, a react-tracked container initialized in `setupApp()` */
|
|
10
|
-
let useTracked: typeof import('./store').useTracked
|
|
11
8
|
|
|
12
9
|
// Common aependency globals
|
|
13
10
|
/** The public API for rendering a history-aware `<a>`. */
|
|
@@ -26,8 +23,6 @@ declare global {
|
|
|
26
23
|
Object.assign(window, {
|
|
27
24
|
// application globals
|
|
28
25
|
onChange: onChange,
|
|
29
|
-
useTracked: useTracked,
|
|
30
|
-
|
|
31
26
|
// dependency globals
|
|
32
27
|
Link: Link,
|
|
33
28
|
useCallback: useCallback,
|
package/client/index.ts
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import '../types/required-globals.d.ts'
|
|
3
3
|
|
|
4
4
|
// export const pi = parseFloat(3.142)
|
|
5
|
-
|
|
5
|
+
// Utility functions
|
|
6
6
|
export * from '../util.js'
|
|
7
7
|
export * as util from '../util.js'
|
|
8
|
+
export * from '../types'
|
|
9
|
+
|
|
10
|
+
// Main app functions
|
|
11
|
+
export { setupApp, updateJwt } from './app'
|
|
12
|
+
export { createStore, exposedStoreData, preloadedStoreData, setStoreWrapper } from './store'
|
|
8
13
|
|
|
9
14
|
// Component Pages
|
|
10
15
|
export { Signin } from '../components/auth/signin'
|
|
@@ -48,5 +53,5 @@ export { Toggle } from '../components/partials/form/toggle'
|
|
|
48
53
|
// Component Other
|
|
49
54
|
export { IsFirstRender } from '../components/partials/is-first-render'
|
|
50
55
|
|
|
51
|
-
//
|
|
52
|
-
export const
|
|
56
|
+
// Expose the injected config
|
|
57
|
+
export const injectedConfig = { ...INJECTED_CONFIG } as import('types').Config
|
package/client/store.ts
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
import { createContainer } from 'react-tracked'
|
|
2
|
+
import { Dispatch, SetStateAction } from 'react'
|
|
3
|
+
import { axios, isObject } from 'nitro-web/util'
|
|
4
|
+
import { updateJwt } from 'nitro-web'
|
|
2
5
|
import { Store } from 'nitro-web/types'
|
|
3
6
|
|
|
7
|
+
export let preloadedStoreData: Store
|
|
8
|
+
export let exposedStoreData: Store
|
|
4
9
|
|
|
5
|
-
export
|
|
10
|
+
export function createStore<T extends Store>(store: T) {
|
|
11
|
+
const container = createContainer(() => {
|
|
12
|
+
// const [state, setState] = useState<T>(() => (initData || store || {}) as T)
|
|
13
|
+
const [state, setState] = useState<T>(() => beforeUpdate((preloadedStoreData || store || {}) as T))
|
|
14
|
+
exposedStoreData = state
|
|
15
|
+
return [state, setStoreWrapper(setState)]
|
|
16
|
+
})
|
|
17
|
+
return container
|
|
18
|
+
}
|
|
6
19
|
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
export function setStoreWrapper<T extends Store>(setState: Dispatch<SetStateAction<T>>, _beforeUpdate?: (newStore: T) => T) {
|
|
21
|
+
_beforeUpdate = _beforeUpdate || beforeUpdate
|
|
22
|
+
return (updater: SetStateAction<T>) => {
|
|
23
|
+
if (typeof updater === 'function') setState((prev: T) => beforeUpdate(updater(prev)))
|
|
24
|
+
else setState(() => beforeUpdate(updater))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
function beforeUpdate<T extends Store>(newStore: T) {
|
|
29
|
+
/**
|
|
30
|
+
* Get store object (called on signup/signin/signout/state)
|
|
31
|
+
* @param {object} <newData> - pass to override store with /login or /state request data
|
|
32
|
+
* @return {object} store
|
|
33
|
+
*/
|
|
34
|
+
if (!newStore || !isObject(newStore)) return newStore
|
|
12
35
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} else {
|
|
18
|
-
setStore((prevStore: Store) => beforeUpdate(prevStore, updater))
|
|
19
|
-
}
|
|
36
|
+
// If newData.jwt is present, update the jwt token
|
|
37
|
+
if (newStore?.jwt) {
|
|
38
|
+
updateJwt(newStore.jwt)
|
|
39
|
+
delete newStore.jwt
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export let exposedData: Store
|
|
27
|
-
export const { Provider, useTracked } = container
|
|
28
|
-
export function beforeCreate(_initData: Store, _beforeUpdate: BeforeUpdate) {
|
|
29
|
-
initData = _initData // normally provided from a /login or /state request data
|
|
30
|
-
beforeUpdate = _beforeUpdate
|
|
42
|
+
// E.g. Cookie matching handy for rare issues, e.g. signout > signin (to a different user on another tab)
|
|
43
|
+
axios().defaults.headers.authid = newStore?.user?._id
|
|
44
|
+
return newStore
|
|
31
45
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import MongoStore from 'connect-mongo'
|
|
3
2
|
import crypto from 'crypto'
|
|
4
|
-
import expressSession from 'express-session'
|
|
5
3
|
import passport from 'passport'
|
|
6
4
|
import passportLocal from 'passport-local'
|
|
5
|
+
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
|
|
7
6
|
import db from 'monastery'
|
|
7
|
+
import jsonwebtoken from 'jsonwebtoken'
|
|
8
8
|
import { sendEmail } from 'nitro-web/server'
|
|
9
9
|
import * as util from 'nitro-web/util'
|
|
10
|
-
// import stripeController from '../billing/stripe.api.js'
|
|
11
10
|
|
|
12
11
|
let config = {}
|
|
12
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
|
|
13
13
|
|
|
14
14
|
export default {
|
|
15
15
|
|
|
@@ -24,43 +24,19 @@ export default {
|
|
|
24
24
|
},
|
|
25
25
|
|
|
26
26
|
setup: function (middleware, _config) {
|
|
27
|
-
// Setup passport handlers for reading and writing to req.session
|
|
28
27
|
const that = this
|
|
29
28
|
global.passport = passport
|
|
30
29
|
|
|
31
30
|
// Set config values
|
|
32
|
-
config = {
|
|
33
|
-
|
|
34
|
-
masterPassword: _config.masterPassword,
|
|
35
|
-
}
|
|
36
|
-
for (let key in config) {
|
|
37
|
-
if (!config[key] && key != 'masterPassword') {
|
|
38
|
-
throw new Error(`Missing config value for stripe.api.js: ${key}`)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// After successful login, serialize the user into a session object
|
|
43
|
-
passport.serializeUser((user, next) => {
|
|
44
|
-
next(null, { _id: user._id })
|
|
45
|
-
})
|
|
31
|
+
config = { env: _config.env, masterPassword: _config.masterPassword }
|
|
32
|
+
if (!config.env) throw new Error('Missing config value for: config.env')
|
|
46
33
|
|
|
47
|
-
// After session read, get the user from the session object
|
|
48
|
-
passport.deserializeUser(async (sessionObject, next) => {
|
|
49
|
-
try {
|
|
50
|
-
const user = await that._findUserFromProvider('deserialize', sessionObject)
|
|
51
|
-
next(null, user)
|
|
52
|
-
} catch (err) {
|
|
53
|
-
next(err.message)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// Setup passport local signin strategy
|
|
58
34
|
passport.use(
|
|
59
35
|
new passportLocal.Strategy(
|
|
60
|
-
{ usernameField: 'email' },
|
|
36
|
+
{ usernameField: 'email' },
|
|
61
37
|
async (email, password, next) => {
|
|
62
38
|
try {
|
|
63
|
-
const user = await that._findUserFromProvider('email', { email
|
|
39
|
+
const user = await that._findUserFromProvider('email', { email, password })
|
|
64
40
|
next(null, user)
|
|
65
41
|
} catch (err) {
|
|
66
42
|
next(err.message)
|
|
@@ -69,58 +45,45 @@ export default {
|
|
|
69
45
|
)
|
|
70
46
|
)
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// });
|
|
93
|
-
// }));
|
|
94
|
-
|
|
95
|
-
// Add session middleware
|
|
96
|
-
middleware.order.splice(3, 0, 'session', 'passport', 'passportSession', 'passportError', 'blocked')
|
|
48
|
+
passport.use(
|
|
49
|
+
new JwtStrategy(
|
|
50
|
+
{
|
|
51
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
52
|
+
secretOrKey: JWT_SECRET,
|
|
53
|
+
},
|
|
54
|
+
async (payload, done) => {
|
|
55
|
+
try {
|
|
56
|
+
const user = await that._findUserFromProvider('deserialize', { _id: payload._id })
|
|
57
|
+
if (!user) return done(null, false)
|
|
58
|
+
return done(null, user)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return done(err, false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
|
|
67
|
+
|
|
97
68
|
Object.assign(middleware, {
|
|
98
69
|
blocked: function (req, res, next) {
|
|
99
70
|
if (req.user && req.user.loginActive === false) {
|
|
100
|
-
|
|
101
|
-
res.error('This user is not available.')
|
|
71
|
+
res.status(403).error('This user is not available.')
|
|
102
72
|
} else {
|
|
103
73
|
next()
|
|
104
74
|
}
|
|
105
75
|
},
|
|
76
|
+
jwtAuth: function(req, res, next) {
|
|
77
|
+
passport.authenticate('jwt', { session: false }, function(err, user) {
|
|
78
|
+
if (user) req.user = user
|
|
79
|
+
next()
|
|
80
|
+
})(req, res, next)
|
|
81
|
+
},
|
|
106
82
|
passport: passport.initialize(),
|
|
107
83
|
passportError: function (err, req, res, next) {
|
|
108
84
|
if (!err) return next()
|
|
109
|
-
req.logout()
|
|
110
85
|
res.error(err)
|
|
111
86
|
},
|
|
112
|
-
passportSession: passport.session(),
|
|
113
|
-
session: expressSession({
|
|
114
|
-
secret: '092720e5ffc1237266b8517239cd81b6', // Changing invalidates cookies
|
|
115
|
-
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 },
|
|
116
|
-
resave: false,
|
|
117
|
-
saveUninitialized: false,
|
|
118
|
-
store: MongoStore.create({
|
|
119
|
-
clientPromise: db.onOpen((manager) => {
|
|
120
|
-
return manager.client
|
|
121
|
-
}),
|
|
122
|
-
}),
|
|
123
|
-
}),
|
|
124
87
|
})
|
|
125
88
|
},
|
|
126
89
|
|
|
@@ -131,31 +94,26 @@ export default {
|
|
|
131
94
|
signup: async function (req, res) {
|
|
132
95
|
try {
|
|
133
96
|
let user = await this._userCreate(req.body)
|
|
134
|
-
// Welcome email
|
|
135
97
|
sendEmail({
|
|
136
98
|
config: config,
|
|
137
99
|
template: 'welcome',
|
|
138
100
|
to: `${util.ucFirst(user.firstName)}<${user.email}>`,
|
|
139
|
-
}).catch(
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
// Login
|
|
143
|
-
res.send(await this._signinAndGetState(req, user))
|
|
101
|
+
}).catch(console.error)
|
|
102
|
+
res.send(await this._signinAndGetState(user, req.query.desktop))
|
|
144
103
|
} catch (err) {
|
|
145
104
|
res.error(err)
|
|
146
105
|
}
|
|
147
106
|
},
|
|
148
107
|
|
|
149
108
|
signin: function (req, res) {
|
|
150
|
-
// console.log('api: signin')
|
|
151
|
-
// console.log(req.body)
|
|
152
109
|
if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
|
|
153
110
|
if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
|
|
111
|
+
|
|
154
112
|
passport.authenticate('local', { session: false }, async (err, user, info) => {
|
|
155
|
-
if (err) return
|
|
113
|
+
if (err) return res.error(err)
|
|
156
114
|
if (!user && info) return res.error('email', info.message)
|
|
157
115
|
try {
|
|
158
|
-
const response = await this._signinAndGetState(
|
|
116
|
+
const response = await this._signinAndGetState(user, req.query.desktop)
|
|
159
117
|
res.send(response)
|
|
160
118
|
} catch (err) {
|
|
161
119
|
res.error(err)
|
|
@@ -164,35 +122,29 @@ export default {
|
|
|
164
122
|
},
|
|
165
123
|
|
|
166
124
|
signout: function (req, res) {
|
|
167
|
-
req.logout()
|
|
168
125
|
res.json('{}')
|
|
169
126
|
},
|
|
170
127
|
|
|
171
128
|
resetInstructions: async function (req, res) {
|
|
172
129
|
try {
|
|
173
|
-
let email = (req.body.email||'').trim().toLowerCase()
|
|
174
|
-
if (!email || !util.isString(email)) {
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
// Find matching user and create new reset token
|
|
178
|
-
let user = await db.user.findOne({ query: { email: email }, _privateData: true })
|
|
130
|
+
let email = (req.body.email || '').trim().toLowerCase()
|
|
131
|
+
if (!email || !util.isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
132
|
+
|
|
133
|
+
let user = await db.user.findOne({ query: { email }, _privateData: true })
|
|
179
134
|
if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Email.
|
|
135
|
+
|
|
136
|
+
let resetToken = await this._tokenCreate(user._id)
|
|
137
|
+
await db.user.update({ query: { email }, $set: { resetToken }})
|
|
138
|
+
|
|
185
139
|
res.json({})
|
|
186
140
|
sendEmail({
|
|
187
141
|
config: config,
|
|
188
142
|
template: 'reset-password',
|
|
189
143
|
to: `${util.ucFirst(user.firstName)}<${email}>`,
|
|
190
144
|
data: {
|
|
191
|
-
token:
|
|
145
|
+
token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
|
|
192
146
|
},
|
|
193
|
-
}).catch(err =>
|
|
194
|
-
console.error('sendEmail(..) mailgun error', err)
|
|
195
|
-
})
|
|
147
|
+
}).catch(err => console.error('sendEmail(..) mailgun error', err))
|
|
196
148
|
} catch (err) {
|
|
197
149
|
res.error(err)
|
|
198
150
|
}
|
|
@@ -202,18 +154,11 @@ export default {
|
|
|
202
154
|
try {
|
|
203
155
|
const { token, password, password2 } = req.body
|
|
204
156
|
const id = this._tokenParse(token)
|
|
205
|
-
// Validate password
|
|
206
157
|
this._validatePassword(password, password2)
|
|
207
|
-
|
|
208
|
-
let user = await db.user.findOne({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
_privateData: true,
|
|
212
|
-
})
|
|
213
|
-
if (!user || user.resetToken !== token) {
|
|
214
|
-
throw new Error('Sorry your email token is invalid or has already been used verify your email.')
|
|
215
|
-
}
|
|
216
|
-
// Update user with new password
|
|
158
|
+
|
|
159
|
+
let user = await db.user.findOne({ query: id, blacklist: ['-resetToken'], _privateData: true })
|
|
160
|
+
if (!user || user.resetToken !== token) throw new Error('Sorry your email token is invalid or has already been used.')
|
|
161
|
+
|
|
217
162
|
await db.user.update({
|
|
218
163
|
query: user._id,
|
|
219
164
|
data: {
|
|
@@ -222,7 +167,7 @@ export default {
|
|
|
222
167
|
},
|
|
223
168
|
blacklist: ['-resetToken', '-password'],
|
|
224
169
|
})
|
|
225
|
-
res.send(await this._signinAndGetState(
|
|
170
|
+
res.send(await this._signinAndGetState({ ...user, resetToken: undefined }, req.query.desktop))
|
|
226
171
|
} catch (err) {
|
|
227
172
|
res.error(err)
|
|
228
173
|
}
|
|
@@ -264,27 +209,19 @@ export default {
|
|
|
264
209
|
/* ---- Private fns ---------------- */
|
|
265
210
|
|
|
266
211
|
_getState: async function (user) {
|
|
267
|
-
//
|
|
268
|
-
return {
|
|
212
|
+
// Initial state
|
|
213
|
+
return {
|
|
269
214
|
user: user || null,
|
|
270
|
-
// stripeProducts: await stripeController._getProducts(),
|
|
271
215
|
}
|
|
272
216
|
},
|
|
273
217
|
|
|
274
|
-
_signinAndGetState: function (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
resolve(await this._getState(user))
|
|
282
|
-
})
|
|
283
|
-
} else {
|
|
284
|
-
return reject('This user is not available.')
|
|
285
|
-
// this._getState().then((state) => resolve(state))
|
|
286
|
-
}
|
|
287
|
-
})
|
|
218
|
+
_signinAndGetState: async function (user, isDesktop) {
|
|
219
|
+
if (user.loginActive === false) throw 'This user is not available.'
|
|
220
|
+
user.desktop = isDesktop
|
|
221
|
+
|
|
222
|
+
const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
|
|
223
|
+
const state = await this._getState(user)
|
|
224
|
+
return { ...state, jwt }
|
|
288
225
|
},
|
|
289
226
|
|
|
290
227
|
_tokenCreate: function (id) {
|
|
@@ -3,13 +3,13 @@ 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()
|
|
7
7
|
const [, setStore] = useTracked()
|
|
8
8
|
const [state, setState] = useState({ email: '', errors: [] as Errors })
|
|
9
9
|
|
|
10
10
|
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
11
11
|
try {
|
|
12
|
-
await util.request(
|
|
12
|
+
await util.request('post /api/reset-instructions', state, event, isLoading)
|
|
13
13
|
setStore(s => ({ ...s, message: 'Done! Please check your email.' }))
|
|
14
14
|
navigate('/signin')
|
|
15
15
|
} catch (e) {
|
|
@@ -32,7 +32,7 @@ export function ResetInstructions() {
|
|
|
32
32
|
<FormError state={state} className="pt-2" />
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
|
-
<Button className="w-full" isLoading={
|
|
35
|
+
<Button className="w-full" isLoading={isLoading[0]} type="submit">Email me a reset password link</Button>
|
|
36
36
|
</form>
|
|
37
37
|
</div>
|
|
38
38
|
)
|
|
@@ -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()
|
|
45
45
|
const [, setStore] = useTracked()
|
|
46
46
|
const [state, setState] = useState(() => ({
|
|
47
47
|
password: '',
|
|
@@ -52,7 +52,7 @@ export function ResetPassword() {
|
|
|
52
52
|
|
|
53
53
|
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
54
54
|
try {
|
|
55
|
-
const data = await util.request(
|
|
55
|
+
const data = await util.request('post /api/reset-password', state, event, isLoading)
|
|
56
56
|
setStore(() => data)
|
|
57
57
|
navigate('/')
|
|
58
58
|
} catch (e) {
|
|
@@ -79,7 +79,7 @@ export function ResetPassword() {
|
|
|
79
79
|
<FormError state={state} className="pt-2" />
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
|
-
<Button class="w-full" isLoading={
|
|
82
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
|
|
83
83
|
</form>
|
|
84
84
|
</div>
|
|
85
85
|
)
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { Topbar, Field, Button, FormError, util } from 'nitro-web'
|
|
2
|
-
import {
|
|
1
|
+
import { Topbar, Field, Button, FormError, util, injectedConfig, updateJwt } from 'nitro-web'
|
|
2
|
+
import { Errors } from 'nitro-web/types'
|
|
3
3
|
|
|
4
|
-
export function Signin(
|
|
4
|
+
export function Signin() {
|
|
5
5
|
const navigate = useNavigate()
|
|
6
6
|
const location = useLocation()
|
|
7
7
|
const isSignout = location.pathname == '/signout'
|
|
8
|
-
const isLoading = useState(isSignout
|
|
8
|
+
const isLoading = useState(isSignout)
|
|
9
9
|
const [, setStore] = useTracked()
|
|
10
10
|
const [state, setState] = useState({
|
|
11
|
-
email:
|
|
12
|
-
password:
|
|
11
|
+
email: injectedConfig.env == 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
12
|
+
password: injectedConfig.env == 'development' ? '1234' : '',
|
|
13
13
|
errors: [] as Errors,
|
|
14
14
|
})
|
|
15
15
|
|
|
@@ -22,17 +22,19 @@ export function Signin({ config }: { config: Config }) {
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
if (isSignout) {
|
|
24
24
|
setStore(() => ({ user: null }))
|
|
25
|
-
util.axios().get('/api/signout')
|
|
26
|
-
|
|
25
|
+
// util.axios().get('/api/signout')
|
|
26
|
+
Promise.resolve()
|
|
27
|
+
.then(() => isLoading[1](false))
|
|
28
|
+
.then(() => updateJwt())
|
|
27
29
|
.then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
|
|
28
|
-
.catch(err => (console.error(err), isLoading[1](
|
|
30
|
+
.catch(err => (console.error(err), isLoading[1](false)))
|
|
29
31
|
}
|
|
30
32
|
}, [isSignout])
|
|
31
33
|
|
|
32
34
|
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
33
35
|
try {
|
|
34
|
-
const data = await util.request(
|
|
35
|
-
isLoading[1](
|
|
36
|
+
const data = await util.request('post /api/signin', state, e, isLoading)
|
|
37
|
+
isLoading[1](true)
|
|
36
38
|
setStore(() => data)
|
|
37
39
|
setTimeout(() => { // wait for setStore
|
|
38
40
|
if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
|
|
@@ -65,7 +67,7 @@ export function Signin({ config }: { config: Config }) {
|
|
|
65
67
|
<FormError state={state} className="pt-2" />
|
|
66
68
|
</div>
|
|
67
69
|
|
|
68
|
-
<Button class="w-full" isLoading={
|
|
70
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
|
|
69
71
|
</form>
|
|
70
72
|
</div>
|
|
71
73
|
)
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { Button, Field, FormError, Topbar, util } from 'nitro-web'
|
|
2
|
-
import {
|
|
1
|
+
import { Button, Field, FormError, Topbar, util, injectedConfig } from 'nitro-web'
|
|
2
|
+
import { Errors } from 'nitro-web/types'
|
|
3
3
|
|
|
4
|
-
export function Signup(
|
|
4
|
+
export function Signup() {
|
|
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({
|
|
9
|
-
email:
|
|
10
|
-
name:
|
|
11
|
-
business: { name:
|
|
12
|
-
password:
|
|
9
|
+
email: injectedConfig.env === 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
10
|
+
name: injectedConfig.env === 'development' ? 'Bruce Wayne' : '',
|
|
11
|
+
business: { name: injectedConfig.env === 'development' ? 'Wayne Enterprises' : '' },
|
|
12
|
+
password: injectedConfig.env === 'development' ? '1234' : '',
|
|
13
13
|
errors: [] as Errors,
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
17
17
|
try {
|
|
18
|
-
const data = await util.request(
|
|
19
|
-
isLoading[1](
|
|
18
|
+
const data = await util.request('post /api/signup', state, e, isLoading)
|
|
19
|
+
isLoading[1](true)
|
|
20
20
|
setStore(() => data)
|
|
21
21
|
setTimeout(() => navigate('/'), 0) // wait for setStore
|
|
22
22
|
} catch (e) {
|
|
@@ -53,7 +53,7 @@ export function Signup({ config }: { config: Config}) {
|
|
|
53
53
|
<FormError state={state} className="pt-2" />
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
|
-
<Button class="w-full" isLoading={
|
|
56
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Create Account</Button>
|
|
57
57
|
</form>
|
|
58
58
|
</div>
|
|
59
59
|
)
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge'
|
|
2
2
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
|
|
3
3
|
|
|
4
|
-
type
|
|
5
|
-
color?: 'primary'|'secondary'|'white'
|
|
4
|
+
type Button = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
5
|
+
color?: 'primary'|'secondary'|'black'|'white'
|
|
6
6
|
size?: 'xs'|'sm'|'md'|'lg'
|
|
7
7
|
className?: string
|
|
8
8
|
isLoading?: boolean
|
|
9
9
|
IconLeft?: React.ReactNode|'v'
|
|
10
|
+
IconLeftEnd?: React.ReactNode|'v'
|
|
10
11
|
IconRight?: React.ReactNode|'v'
|
|
11
|
-
|
|
12
|
+
IconRightEnd?: React.ReactNode|'v'
|
|
12
13
|
children?: React.ReactNode|'v'
|
|
13
|
-
[key: string]: unknown
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function Button({
|
|
16
|
+
export function Button({
|
|
17
|
+
size='md',
|
|
18
|
+
color='primary',
|
|
19
|
+
className,
|
|
20
|
+
isLoading,
|
|
21
|
+
IconLeft,
|
|
22
|
+
IconLeftEnd,
|
|
23
|
+
IconRight,
|
|
24
|
+
IconRightEnd,
|
|
25
|
+
children,
|
|
26
|
+
...props
|
|
27
|
+
}: Button) {
|
|
17
28
|
// const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
|
|
18
|
-
const iconPosition = IconLeft ? 'left' : IconRight ? 'right' :
|
|
19
|
-
const base =
|
|
29
|
+
const iconPosition = IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : 'none'
|
|
30
|
+
const base =
|
|
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'
|
|
20
33
|
|
|
21
|
-
// Button
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
// Button colors, you can use custom colors by using className instead
|
|
35
|
+
const colors = {
|
|
36
|
+
primary: 'bg-primary hover:bg-primary-hover',
|
|
37
|
+
secondary: 'bg-secondary hover:bg-secondary-hover',
|
|
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',
|
|
40
|
+
}
|
|
25
41
|
|
|
26
42
|
// Button sizes
|
|
27
43
|
const sizes = {
|
|
@@ -31,20 +47,7 @@ export function Button({ color='primary', size='md', className, isLoading, IconL
|
|
|
31
47
|
lg: 'px-3.5 py-2.5 text-sm rounded-md',
|
|
32
48
|
}
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
const contentLayouts = {
|
|
36
|
-
left: 'w-full inline-flex items-center gap-x-1.5',
|
|
37
|
-
right: 'w-full inline-flex items-center gap-x-1.5',
|
|
38
|
-
right2: 'w-full inline-flex items-center justify-between gap-x-1.5',
|
|
39
|
-
none: 'w-full gap-x-1.5',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let colorAndSize = ''
|
|
43
|
-
if (color.match(/primary/)) colorAndSize = `${primary} ${sizes[size]}`
|
|
44
|
-
else if (color.match(/secondary/)) colorAndSize = `${secondary} ${sizes[size]}`
|
|
45
|
-
else if (color.match(/white/)) colorAndSize = `${white} ${sizes[size]}`
|
|
46
|
-
|
|
47
|
-
const contentLayout = `${contentLayouts[iconPosition]}`
|
|
50
|
+
const contentLayout = `w-full gap-x-1.5 ${iconPosition == 'none' ? '' : 'inline-flex items-center justify-center'}`
|
|
48
51
|
const loading = isLoading ? '[&>*]:opacity-0 text-opacity-0' : ''
|
|
49
52
|
|
|
50
53
|
function getIcon(Icon: React.ReactNode | 'v') {
|
|
@@ -54,16 +57,18 @@ export function Button({ color='primary', size='md', className, isLoading, IconL
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
|
-
<button class={twMerge(`${base} ${
|
|
60
|
+
<button class={twMerge(`${base} ${colors[color]} ${sizes[size]} ${contentLayout} ${loading} ${className||''}`)} {...props}>
|
|
58
61
|
{IconLeft && getIcon(IconLeft)}
|
|
59
|
-
{
|
|
62
|
+
{IconLeftEnd && getIcon(IconLeftEnd)}
|
|
63
|
+
<span class={`${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1' : ''}`}>{children}</span>
|
|
60
64
|
{IconRight && getIcon(IconRight)}
|
|
61
|
-
{
|
|
65
|
+
{IconRightEnd && getIcon(IconRightEnd)}
|
|
62
66
|
{
|
|
63
|
-
isLoading &&
|
|
64
|
-
<span
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
isLoading &&
|
|
68
|
+
<span className={
|
|
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'
|
|
71
|
+
} />
|
|
67
72
|
}
|
|
68
73
|
</button>
|
|
69
74
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
|
|
2
2
|
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
|
|
3
3
|
import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
|
|
4
|
-
import {
|
|
4
|
+
import { injectedConfig } from 'nitro-web'
|
|
5
5
|
import {
|
|
6
6
|
Bars3Icon,
|
|
7
7
|
HomeIcon,
|
|
@@ -16,14 +16,13 @@ export type SidebarProps = {
|
|
|
16
16
|
Logo?: React.FC<{ width?: string, height?: string }>;
|
|
17
17
|
menu?: { name: string; to: string; Icon: React.FC<{ className?: string }> }[]
|
|
18
18
|
links?: { name: string; to: string; initial: string }[]
|
|
19
|
-
version?: string
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function classNames(...classes: string[]) {
|
|
23
22
|
return classes.filter(Boolean).join(' ')
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
export function Sidebar({ Logo, menu, links
|
|
25
|
+
export function Sidebar({ Logo, menu, links }: SidebarProps) {
|
|
27
26
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
28
27
|
return (
|
|
29
28
|
<>
|
|
@@ -46,14 +45,14 @@ export function Sidebar({ Logo, menu, links, version }: SidebarProps) {
|
|
|
46
45
|
</button>
|
|
47
46
|
</div>
|
|
48
47
|
</TransitionChild>
|
|
49
|
-
<SidebarContents Logo={Logo} menu={menu} links={links}
|
|
48
|
+
<SidebarContents Logo={Logo} menu={menu} links={links} />
|
|
50
49
|
</DialogPanel>
|
|
51
50
|
</div>
|
|
52
51
|
</Dialog>
|
|
53
52
|
|
|
54
53
|
{/* Static sidebar for desktop */}
|
|
55
54
|
<div className={`hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col ${sidebarWidth}`}>
|
|
56
|
-
<SidebarContents Logo={Logo} menu={menu} links={links}
|
|
55
|
+
<SidebarContents Logo={Logo} menu={menu} links={links} />
|
|
57
56
|
</div>
|
|
58
57
|
|
|
59
58
|
{/* mobile sidebar closed */}
|
|
@@ -72,7 +71,7 @@ export function Sidebar({ Logo, menu, links, version }: SidebarProps) {
|
|
|
72
71
|
)
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
function SidebarContents ({ Logo, menu, links
|
|
74
|
+
function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
76
75
|
const location = useLocation()
|
|
77
76
|
const [store] = useTracked()
|
|
78
77
|
const user = store.user
|
|
@@ -85,7 +84,7 @@ function SidebarContents ({ Logo, menu, links, version }: SidebarProps) {
|
|
|
85
84
|
|
|
86
85
|
const _menu = menu || [
|
|
87
86
|
{ name: 'Dashboard', to: '/', Icon: HomeIcon },
|
|
88
|
-
{ name: isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
|
|
87
|
+
{ name: injectedConfig.isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
|
|
89
88
|
{ name: 'Pricing', to: '/pricing', Icon: UsersIcon },
|
|
90
89
|
{ name: 'Signout', to: '/signout', Icon: ArrowLeftCircleIcon },
|
|
91
90
|
]
|
|
@@ -102,7 +101,7 @@ function SidebarContents ({ Logo, menu, links, version }: SidebarProps) {
|
|
|
102
101
|
<Link to="/">
|
|
103
102
|
<Logo width="70" height={undefined} />
|
|
104
103
|
</Link>
|
|
105
|
-
<span className="text-[9px] text-gray-900 font-semibold mt-4">{version}</span>
|
|
104
|
+
<span className="text-[9px] text-gray-900 font-semibold mt-4">{injectedConfig.version}</span>
|
|
106
105
|
</div>
|
|
107
106
|
)}
|
|
108
107
|
<nav className="flex flex-1 flex-col">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink,
|
|
1
|
+
import { Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink, Modal, Calendar, injectedConfig } from 'nitro-web'
|
|
2
2
|
import { getCountryOptions, getCurrencyOptions, ucFirst } from 'nitro-web/util'
|
|
3
3
|
import { CheckIcon } from '@heroicons/react/20/solid'
|
|
4
4
|
import { Config } from 'nitro-web/types'
|
|
@@ -55,7 +55,7 @@ export function Styleguide({ config }: { config: Config }) {
|
|
|
55
55
|
<div class="mb-10 text-left max-w-[1100px]">
|
|
56
56
|
<GithubLink filename={__filename} />
|
|
57
57
|
<div class="mb-7">
|
|
58
|
-
<h1 class="h1">{isDemo ? 'Design System' : 'Style Guide'}</h1>
|
|
58
|
+
<h1 class="h1">{injectedConfig.isDemo ? 'Design System' : 'Style Guide'}</h1>
|
|
59
59
|
<p>
|
|
60
60
|
Components are styled using
|
|
61
61
|
<a href="https://v3.tailwindcss.com/docs/configuration" class="underline" target="_blank" rel="noreferrer">TailwindCSS</a>.
|
|
@@ -88,12 +88,12 @@ export function Styleguide({ config }: { config: Config }) {
|
|
|
88
88
|
minWidth="330px"
|
|
89
89
|
options={[{ label: <><b>New Customer</b> / Add <b>Bruce Lee</b></>, className: 'border-bottom-with-space' }, ...options]}
|
|
90
90
|
>
|
|
91
|
-
<Button
|
|
91
|
+
<Button color="white" IconRight="v" class="gap-x-3">Dropdown bottom-right</Button>
|
|
92
92
|
</Dropdown>
|
|
93
93
|
</div>
|
|
94
94
|
<div>
|
|
95
95
|
<Dropdown options={options} dir="top-left" minWidth="250px">
|
|
96
|
-
<Button
|
|
96
|
+
<Button color="white" IconRight="v" class="gap-x-3">Dropdown top-left</Button>
|
|
97
97
|
</Dropdown>
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
@@ -107,9 +107,11 @@ export function Styleguide({ config }: { config: Config }) {
|
|
|
107
107
|
<div><Button color="primary" size="sm">*-sm button</Button></div>
|
|
108
108
|
<div><Button color="primary">*-md (default)</Button></div>
|
|
109
109
|
<div><Button color="primary" size="lg">*-lg button</Button></div>
|
|
110
|
-
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />}>IconLeft
|
|
111
|
-
<div><Button
|
|
112
|
-
<div><Button
|
|
110
|
+
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />}>IconLeft</Button></div>
|
|
111
|
+
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />} className="w-[160px]">IconLeft 160px</Button></div>
|
|
112
|
+
<div><Button IconLeftEnd={<CheckIcon class="size-5 -my-5 -mx-0.5" />} className="w-[190px]">IconLeftEnd 190px</Button></div>
|
|
113
|
+
<div><Button IconRight="v">IconRight</Button></div>
|
|
114
|
+
<div><Button IconRightEnd="v" className="w-[190px]">IconRightEnd 190px</Button></div>
|
|
113
115
|
<div><Button color="primary" IconRight="v" isLoading>primary isLoading</Button></div>
|
|
114
116
|
</div>
|
|
115
117
|
|
|
@@ -5,7 +5,7 @@ import SvgTick from 'nitro-web/client/imgs/icons/tick.svg'
|
|
|
5
5
|
import { Button, FormError, Field, Modal, Topbar, Tabbar } from 'nitro-web'
|
|
6
6
|
|
|
7
7
|
export function SettingsAccount() {
|
|
8
|
-
const isLoading = useState(
|
|
8
|
+
const isLoading = useState(false)
|
|
9
9
|
const [removeModal, setRemoveModal] = useState()
|
|
10
10
|
const [{user}, setStore] = sharedStore.useTracked()
|
|
11
11
|
const [state, setState] = useState({
|
|
@@ -17,7 +17,7 @@ export function SettingsAccount() {
|
|
|
17
17
|
|
|
18
18
|
async function onSubmit (e) {
|
|
19
19
|
try {
|
|
20
|
-
const res = await util.request(
|
|
20
|
+
const res = await util.request(`put /api/user/${user._id}?files=true`, state, e, isLoading)
|
|
21
21
|
setStore((s) => ({ ...s, user: { ...s.user, ...res }, message: 'Saved successfully 👍️' }))
|
|
22
22
|
} catch (errors) {
|
|
23
23
|
return setState({ ...state, errors })
|
|
@@ -92,7 +92,7 @@ export function RemoveModal ({ show, setShow }) {
|
|
|
92
92
|
|
|
93
93
|
async function onSubmit (e) {
|
|
94
94
|
try {
|
|
95
|
-
await util.request(
|
|
95
|
+
await util.request(`delete /api/account/${state._id}`, null, e, isLoading)
|
|
96
96
|
close()
|
|
97
97
|
setStore(o => ({ ...o, message: 'Data deleted successfully, Goodbye 👋...' }))
|
|
98
98
|
setTimeout(() => navigate('/signout'), 6000) // wait for setStore
|
|
@@ -7,7 +7,7 @@ import SvgTick from 'nitro-web/client/imgs/icons/tick.svg'
|
|
|
7
7
|
import { Button, Field, Select, Topbar, Tabbar } from 'nitro-web'
|
|
8
8
|
|
|
9
9
|
export function SettingsBusiness({ config }) {
|
|
10
|
-
const isLoading = useState(
|
|
10
|
+
const isLoading = useState(false)
|
|
11
11
|
const [{ user }, setStore] = sharedStore.useTracked()
|
|
12
12
|
const [state, setState] = useState(() => {
|
|
13
13
|
const company = user.company
|
|
@@ -26,7 +26,7 @@ export function SettingsBusiness({ config }) {
|
|
|
26
26
|
|
|
27
27
|
async function onSubmit (e) {
|
|
28
28
|
try {
|
|
29
|
-
const company = await util.request(
|
|
29
|
+
const company = await util.request(`put /api/company/${user.company._id}`, state, e, isLoading)
|
|
30
30
|
setStore((s) => ({ ...s, user: { ...s.user, company }, message: 'Saved successfully 👍️' }))
|
|
31
31
|
} catch (errors) {
|
|
32
32
|
console.log(errors)
|
|
@@ -13,7 +13,7 @@ type SettingsTeamMemberProps = {
|
|
|
13
13
|
export function SettingsTeamMember ({ showModal, setShowModal, config }: SettingsTeamMemberProps) {
|
|
14
14
|
// @param {object} showModal - user
|
|
15
15
|
const [{ user }] = sharedStore.useTracked()
|
|
16
|
-
const [isLoading] = useState(
|
|
16
|
+
const [isLoading] = useState(false)
|
|
17
17
|
const [state, setState] = useState({
|
|
18
18
|
business: {
|
|
19
19
|
name: '',
|
|
@@ -5,7 +5,7 @@ import SvgPlus from 'nitro-web/client/imgs/icons/plus.svg'
|
|
|
5
5
|
import { Button, Table, Avatar, Tabbar, Topbar, SettingsTeamMember } from 'nitro-web'
|
|
6
6
|
|
|
7
7
|
export function SettingsTeam({ config }) {
|
|
8
|
-
const isLoading = useState(
|
|
8
|
+
const isLoading = useState(false)
|
|
9
9
|
const [showModal, setShowModal] = useState()
|
|
10
10
|
const [{ user }] = sharedStore.useTracked()
|
|
11
11
|
const [state] = useState({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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 🚀",
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
"bcrypt": "^5.0.0",
|
|
34
34
|
"body-parser": "^1.19.0",
|
|
35
35
|
"compression": "^1.7.4",
|
|
36
|
-
"connect-mongo": "^5.1.0",
|
|
37
36
|
"date-fns": "^3.6.0",
|
|
38
37
|
"dateformat": "^3.0.3",
|
|
39
38
|
"dotenv": "^14.3.2",
|
|
40
39
|
"express": "^4.17.1",
|
|
41
40
|
"express-fileupload": "^1.1.6",
|
|
42
|
-
"express-session": "^1.17.0",
|
|
43
41
|
"inline-css": "^4.0.2",
|
|
42
|
+
"jsonwebtoken": "^9.0.2",
|
|
44
43
|
"nodemailer": "^6.5.0",
|
|
45
44
|
"nodemailer-mailgun-transport": "^2.0.2",
|
|
46
45
|
"nunjucks": "^3.2.2",
|
|
47
46
|
"passport": "^0.4.1",
|
|
47
|
+
"passport-jwt": "^4.0.1",
|
|
48
48
|
"passport-local": "^1.0.0",
|
|
49
49
|
"sort-route-addresses-nodeps": "0.0.4"
|
|
50
50
|
},
|
|
@@ -7,7 +7,6 @@ import { CSSInterpolation } from '@emotion/serialize'
|
|
|
7
7
|
declare global {
|
|
8
8
|
/** Webpack injected config variables */
|
|
9
9
|
const INJECTED_CONFIG: Record<string, string|boolean|object>
|
|
10
|
-
const ISDEMO: boolean
|
|
11
10
|
/** Webpack svg loader */
|
|
12
11
|
module '*.svg' {
|
|
13
12
|
const content: React.FC<React.SVGProps<SVGElement>>
|
package/types/util.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export function pad(num: any, padLeft: any, fixedRight: any): any;
|
|
|
158
158
|
export function pick(obj: any, keys: any): {};
|
|
159
159
|
export function queryObject(search: any, assignTrue: any): any;
|
|
160
160
|
export function queryString(obj: any): string;
|
|
161
|
-
export function request(
|
|
161
|
+
export function request(route: any, data: any, event: any, isLoading: any): Promise<any>;
|
|
162
162
|
export function removeUndefined(variable: any): any;
|
|
163
163
|
export function s3Image(awsUrl: any, image: any, size: string, i: any): any;
|
|
164
164
|
export function sanitizeHTML(string: any): string;
|
package/types.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type InjectedConfig = {
|
|
2
|
+
awsUrl?: string
|
|
3
3
|
clientUrl: string
|
|
4
4
|
countries: { [key: string]: { numberFormats: { currency: string } } } // for input-currency.tsx
|
|
5
5
|
currencies: { [key: string]: { symbol: string, digits: number } } // for input-currency.tsx
|
|
6
6
|
env: string
|
|
7
|
+
googleMapsApiKey?: string
|
|
8
|
+
isDemo: boolean // implicitly defined by webpack
|
|
9
|
+
isStatic: boolean // implicitly defined by webpack
|
|
10
|
+
jwtName: string // implicitly defined by webpack
|
|
7
11
|
name: string
|
|
12
|
+
placeholderEmail?: string
|
|
13
|
+
stripePublishableKey?: string
|
|
14
|
+
titleSeparator?: string
|
|
8
15
|
version: string
|
|
16
|
+
}
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
export type Config = InjectedConfig & {
|
|
19
|
+
// Non-injectable config on the client
|
|
11
20
|
beforeApp?: () => Promise<object>
|
|
12
21
|
beforeStoreUpdate?: (prevStore: Store | null, newData: Store) => Store
|
|
13
|
-
googleMapsApiKey?: string
|
|
14
|
-
isStatic?: boolean
|
|
15
22
|
middleware?: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
|
|
16
|
-
placeholderEmail?: string
|
|
17
|
-
stripePublishableKey?: string
|
|
18
|
-
titleSeparator?: string
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export type User = {
|
|
@@ -45,6 +49,7 @@ export type Store = {
|
|
|
45
49
|
message?: MessageObject | string | null
|
|
46
50
|
user?: User | null,
|
|
47
51
|
apiAvailable?: boolean
|
|
52
|
+
jwt?: string
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export type Svg = React.FC<React.SVGProps<SVGElement>>
|
package/util.js
CHANGED
|
@@ -908,12 +908,12 @@ export function queryString (obj) {
|
|
|
908
908
|
return qs ? `?${qs}` : ''
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
-
export async function request (
|
|
911
|
+
export async function request (route, data, event, isLoading) {
|
|
912
912
|
/**
|
|
913
913
|
* Axios request to the route
|
|
914
|
-
* @param {Event} event - event to prevent default
|
|
915
914
|
* @param {string} route - e.g. 'post /api/user'
|
|
916
915
|
* @param {object} <data> - payload
|
|
916
|
+
* @param {Event} <event> - event to prevent default
|
|
917
917
|
* @param {array} <isLoading> - [isLoading, setIsLoading]
|
|
918
918
|
* @return {promise}
|
|
919
919
|
*/
|
|
@@ -925,7 +925,7 @@ export async function request (event, route, data, isLoading) {
|
|
|
925
925
|
// show loading
|
|
926
926
|
if (isLoading) {
|
|
927
927
|
if (isLoading[0]) return
|
|
928
|
-
else isLoading[1](
|
|
928
|
+
else isLoading[1](true)
|
|
929
929
|
}
|
|
930
930
|
|
|
931
931
|
// warning, not persisting through re-renders, but should be fine until loading is finished
|
|
@@ -950,7 +950,7 @@ export async function request (event, route, data, isLoading) {
|
|
|
950
950
|
])
|
|
951
951
|
|
|
952
952
|
// success
|
|
953
|
-
if (isLoading) isLoading[1](
|
|
953
|
+
if (isLoading) isLoading[1](false)
|
|
954
954
|
if (res.status == 'rejected') throw res.reason
|
|
955
955
|
return res.value.data
|
|
956
956
|
|