@bloom-housing/ui-components 4.4.1-alpha.7 → 5.0.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/CHANGELOG.md +949 -0
- package/index.ts +8 -6
- package/package.json +10 -3
- package/src/actions/Button.tsx +2 -2
- package/src/actions/ExpandableContent.tsx +9 -5
- package/src/blocks/ImageCard.tsx +3 -3
- package/src/blocks/StandardCard.docs.mdx +34 -0
- package/src/blocks/StandardCard.scss +33 -0
- package/src/blocks/StandardCard.tsx +37 -0
- package/src/config/index.ts +0 -1
- package/src/forms/FieldGroup.tsx +1 -1
- package/src/forms/HouseholdSizeField.tsx +2 -1
- package/src/global/tables.scss +7 -1
- package/src/helpers/formOptions.tsx +0 -9
- package/src/helpers/preferences.tsx +3 -314
- package/src/icons/Icon.tsx +22 -3
- package/src/locales/es.json +18 -0
- package/src/locales/general.json +23 -0
- package/src/{prototypes → navigation}/SideNav.scss +15 -9
- package/src/navigation/SideNav.tsx +39 -0
- package/src/notifications/ApplicationStatus.tsx +2 -2
- package/src/overlays/Drawer.tsx +1 -1
- package/src/overlays/LoadingOverlay.tsx +1 -1
- package/src/overlays/Modal.scss +5 -0
- package/src/overlays/Modal.tsx +19 -3
- package/src/page_components/listing/ListingsGroup.tsx +2 -2
- package/src/page_components/listing/listing_sidebar/ExpandableSection.tsx +34 -0
- package/src/page_components/listing/listing_sidebar/QuantityRowSection.tsx +1 -0
- package/src/page_components/sign-in/FormSignInMFACode.tsx +7 -3
- package/src/page_components/sign-in/FormSignInMFAType.tsx +7 -8
- package/src/page_components/sign-in/FormTerms.tsx +9 -27
- package/src/{global/vendor → tables}/AgPagination.tsx +5 -1
- package/src/tables/AgTable.tsx +242 -0
- package/src/tables/StandardTable.tsx +17 -4
- package/src/authentication/AuthContext.ts +0 -386
- package/src/authentication/RequireLogin.tsx +0 -89
- package/src/authentication/index.ts +0 -5
- package/src/authentication/timeout.tsx +0 -128
- package/src/authentication/token.ts +0 -17
- package/src/authentication/useRequireLoggedInUser.ts +0 -19
- package/src/config/ConfigContext.tsx +0 -36
- package/src/helpers/tableSummaries.tsx +0 -104
- package/src/notifications/index.ts +0 -4
- package/src/page_components/listing/UnitTables.tsx +0 -122
- package/src/page_components/listing/listing_sidebar/WhatToExpect.tsx +0 -22
- package/src/prototypes/SideNav.tsx +0 -14
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApplicationsService,
|
|
3
|
-
ApplicationFlaggedSetsService,
|
|
4
|
-
AuthService,
|
|
5
|
-
ListingsService,
|
|
6
|
-
User,
|
|
7
|
-
UserBasic,
|
|
8
|
-
UserCreate,
|
|
9
|
-
UserService,
|
|
10
|
-
UserProfileService,
|
|
11
|
-
serviceOptions,
|
|
12
|
-
Status,
|
|
13
|
-
AmiChartsService,
|
|
14
|
-
ReservedCommunityTypesService,
|
|
15
|
-
UnitAccessibilityPriorityTypesService,
|
|
16
|
-
UnitTypesService,
|
|
17
|
-
PreferencesService,
|
|
18
|
-
JurisdictionsService,
|
|
19
|
-
ProgramsService,
|
|
20
|
-
RequestMfaCodeResponse,
|
|
21
|
-
EnumRequestMfaCodeMfaType,
|
|
22
|
-
EnumLoginMfaType,
|
|
23
|
-
} from "@bloom-housing/backend-core/types"
|
|
24
|
-
import {
|
|
25
|
-
createContext,
|
|
26
|
-
createElement,
|
|
27
|
-
FunctionComponent,
|
|
28
|
-
useContext,
|
|
29
|
-
useEffect,
|
|
30
|
-
useMemo,
|
|
31
|
-
useReducer,
|
|
32
|
-
useCallback,
|
|
33
|
-
} from "react"
|
|
34
|
-
import qs from "qs"
|
|
35
|
-
import axiosStatic from "axios"
|
|
36
|
-
import { ConfigContext } from "../config/ConfigContext"
|
|
37
|
-
import { createAction, createReducer } from "typesafe-actions"
|
|
38
|
-
import { clearToken, getToken, getTokenTtl, setToken } from "./token"
|
|
39
|
-
import { NavigationContext } from "../config/NavigationContext"
|
|
40
|
-
|
|
41
|
-
type ContextProps = {
|
|
42
|
-
amiChartsService: AmiChartsService
|
|
43
|
-
applicationsService: ApplicationsService
|
|
44
|
-
applicationFlaggedSetsService: ApplicationFlaggedSetsService
|
|
45
|
-
listingsService: ListingsService
|
|
46
|
-
jurisdictionsService: JurisdictionsService
|
|
47
|
-
userService: UserService
|
|
48
|
-
userProfileService: UserProfileService
|
|
49
|
-
authService: AuthService
|
|
50
|
-
preferencesService: PreferencesService
|
|
51
|
-
programsService: ProgramsService
|
|
52
|
-
reservedCommunityTypeService: ReservedCommunityTypesService
|
|
53
|
-
unitPriorityService: UnitAccessibilityPriorityTypesService
|
|
54
|
-
unitTypesService: UnitTypesService
|
|
55
|
-
loadProfile: (redirect?: string) => void
|
|
56
|
-
login: (
|
|
57
|
-
email: string,
|
|
58
|
-
password: string,
|
|
59
|
-
mfaCode?: string,
|
|
60
|
-
mfaType?: EnumLoginMfaType
|
|
61
|
-
) => Promise<User | undefined>
|
|
62
|
-
loginWithToken: (token: string) => Promise<User | undefined>
|
|
63
|
-
resetPassword: (
|
|
64
|
-
token: string,
|
|
65
|
-
password: string,
|
|
66
|
-
passwordConfirmation: string
|
|
67
|
-
) => Promise<User | undefined>
|
|
68
|
-
signOut: () => void
|
|
69
|
-
confirmAccount: (token: string) => Promise<User | undefined>
|
|
70
|
-
forgotPassword: (email: string) => Promise<string | undefined>
|
|
71
|
-
createUser: (user: UserCreate) => Promise<UserBasic | undefined>
|
|
72
|
-
resendConfirmation: (email: string) => Promise<Status | undefined>
|
|
73
|
-
accessToken?: string
|
|
74
|
-
initialStateLoaded: boolean
|
|
75
|
-
loading: boolean
|
|
76
|
-
profile?: User
|
|
77
|
-
requestMfaCode: (
|
|
78
|
-
email: string,
|
|
79
|
-
password: string,
|
|
80
|
-
mfaType: EnumRequestMfaCodeMfaType,
|
|
81
|
-
phoneNumber?: string
|
|
82
|
-
) => Promise<RequestMfaCodeResponse | undefined>
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Internal Provider State
|
|
86
|
-
type AuthState = {
|
|
87
|
-
accessToken?: string
|
|
88
|
-
initialStateLoaded: boolean
|
|
89
|
-
language?: string
|
|
90
|
-
loading: boolean
|
|
91
|
-
profile?: User
|
|
92
|
-
refreshTimer?: number
|
|
93
|
-
storageType: string
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
type DispatchType = (...arg: [unknown]) => void
|
|
97
|
-
|
|
98
|
-
// State Mutation Actions
|
|
99
|
-
const saveToken = createAction("SAVE_TOKEN")<{
|
|
100
|
-
apiUrl: string
|
|
101
|
-
accessToken: string
|
|
102
|
-
dispatch: DispatchType
|
|
103
|
-
}>()
|
|
104
|
-
const saveProfile = createAction("SAVE_PROFILE")<User | null>()
|
|
105
|
-
const startLoading = createAction("START_LOADING")()
|
|
106
|
-
const stopLoading = createAction("STOP_LOADING")()
|
|
107
|
-
const signOut = createAction("SIGN_OUT")()
|
|
108
|
-
|
|
109
|
-
const scheduleTokenRefresh = (accessToken: string, onRefresh: (accessToken: string) => void) => {
|
|
110
|
-
const ttl = getTokenTtl(accessToken)
|
|
111
|
-
|
|
112
|
-
if (ttl < 0) {
|
|
113
|
-
// If ttl is negative, then our token is already expired, we'll have to re-login to get a new token.
|
|
114
|
-
//dispatch(signOut())
|
|
115
|
-
return null
|
|
116
|
-
} else {
|
|
117
|
-
// Queue up a refresh for ~1 minute before the token expires
|
|
118
|
-
return (setTimeout(() => {
|
|
119
|
-
const run = async () => {
|
|
120
|
-
const reposne = await new AuthService().token()
|
|
121
|
-
if (reposne) {
|
|
122
|
-
onRefresh(reposne.accessToken)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
void run()
|
|
126
|
-
}, Math.max(ttl - 60000, 0)) as unknown) as number
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const reducer = createReducer(
|
|
130
|
-
{
|
|
131
|
-
loading: false,
|
|
132
|
-
initialStateLoaded: false,
|
|
133
|
-
storageType: "session",
|
|
134
|
-
language: "en",
|
|
135
|
-
} as AuthState,
|
|
136
|
-
{
|
|
137
|
-
SAVE_TOKEN: (state, { payload }) => {
|
|
138
|
-
const { refreshTimer: oldRefresh, ...rest } = state
|
|
139
|
-
const { accessToken, apiUrl, dispatch } = payload
|
|
140
|
-
|
|
141
|
-
// If an existing refresh timer has been defined, remove it as the access token has changed
|
|
142
|
-
if (oldRefresh) {
|
|
143
|
-
clearTimeout(oldRefresh)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Save off the token in local storage for persistence across reloads.
|
|
147
|
-
setToken(state.storageType, accessToken)
|
|
148
|
-
|
|
149
|
-
const refreshTimer = scheduleTokenRefresh(accessToken, (newAccessToken) =>
|
|
150
|
-
dispatch(saveToken({ apiUrl, accessToken: newAccessToken, dispatch }))
|
|
151
|
-
)
|
|
152
|
-
serviceOptions.axios = axiosStatic.create({
|
|
153
|
-
baseURL: apiUrl,
|
|
154
|
-
headers: {
|
|
155
|
-
language: state.language,
|
|
156
|
-
jurisdictionName: process.env.jurisdictionName,
|
|
157
|
-
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
...rest,
|
|
163
|
-
...(refreshTimer && { refreshTimer }),
|
|
164
|
-
accessToken: accessToken,
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
SAVE_PROFILE: (state, { payload: user }) => ({ ...state, profile: user }),
|
|
168
|
-
START_LOADING: (state) => ({ ...state, loading: true }),
|
|
169
|
-
END_LOADING: (state) => ({ ...state, loading: false }),
|
|
170
|
-
SIGN_OUT: ({ storageType }) => {
|
|
171
|
-
clearToken(storageType)
|
|
172
|
-
// Clear out all existing state other than the storage type
|
|
173
|
-
return { loading: false, storageType, initialStateLoaded: true }
|
|
174
|
-
},
|
|
175
|
-
}
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
export const AuthContext = createContext<Partial<ContextProps>>({})
|
|
179
|
-
export const AuthProvider: FunctionComponent = ({ children }) => {
|
|
180
|
-
const { apiUrl, storageType } = useContext(ConfigContext)
|
|
181
|
-
const { router } = useContext(NavigationContext)
|
|
182
|
-
const [state, dispatch] = useReducer(reducer, {
|
|
183
|
-
loading: false,
|
|
184
|
-
initialStateLoaded: false,
|
|
185
|
-
storageType,
|
|
186
|
-
language: router.locale,
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const userService = useMemo(() => new UserService(), [])
|
|
190
|
-
const authService = new AuthService()
|
|
191
|
-
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
serviceOptions.axios = axiosStatic.create({
|
|
194
|
-
baseURL: apiUrl,
|
|
195
|
-
headers: {
|
|
196
|
-
language: router.locale,
|
|
197
|
-
jurisdictionName: process.env.jurisdictionName,
|
|
198
|
-
appUrl: window.location.origin,
|
|
199
|
-
...(state.accessToken && { Authorization: `Bearer ${state.accessToken}` }),
|
|
200
|
-
},
|
|
201
|
-
paramsSerializer: (params) => {
|
|
202
|
-
return qs.stringify(params)
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
}, [router, apiUrl, state.accessToken, router.locale])
|
|
206
|
-
|
|
207
|
-
// On initial load/reload, check localStorage to see if we have a token available
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
const accessToken = getToken(storageType)
|
|
210
|
-
if (accessToken) {
|
|
211
|
-
const ttl = getTokenTtl(accessToken)
|
|
212
|
-
|
|
213
|
-
if (ttl > 0) {
|
|
214
|
-
dispatch(saveToken({ accessToken, apiUrl, dispatch }))
|
|
215
|
-
} else {
|
|
216
|
-
dispatch(signOut())
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
dispatch(signOut())
|
|
220
|
-
}
|
|
221
|
-
}, [apiUrl, storageType])
|
|
222
|
-
|
|
223
|
-
const loadProfile = useCallback(
|
|
224
|
-
async (redirect?: string) => {
|
|
225
|
-
try {
|
|
226
|
-
const profile = await userService?.userControllerProfile()
|
|
227
|
-
if (profile) {
|
|
228
|
-
dispatch(saveProfile(profile))
|
|
229
|
-
}
|
|
230
|
-
} finally {
|
|
231
|
-
dispatch(stopLoading())
|
|
232
|
-
|
|
233
|
-
if (redirect) {
|
|
234
|
-
router.push(redirect)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
[userService, router]
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
// Load our profile as soon as we have an access token available
|
|
242
|
-
useEffect(() => {
|
|
243
|
-
if (!state.profile && state.accessToken && !state.loading) {
|
|
244
|
-
void loadProfile()
|
|
245
|
-
}
|
|
246
|
-
}, [state.profile, state.accessToken, apiUrl, userService, state.loading, loadProfile])
|
|
247
|
-
|
|
248
|
-
const contextValues: ContextProps = {
|
|
249
|
-
amiChartsService: new AmiChartsService(),
|
|
250
|
-
applicationsService: new ApplicationsService(),
|
|
251
|
-
applicationFlaggedSetsService: new ApplicationFlaggedSetsService(),
|
|
252
|
-
listingsService: new ListingsService(),
|
|
253
|
-
jurisdictionsService: new JurisdictionsService(),
|
|
254
|
-
userService: new UserService(),
|
|
255
|
-
userProfileService: new UserProfileService(),
|
|
256
|
-
authService: new AuthService(),
|
|
257
|
-
preferencesService: new PreferencesService(),
|
|
258
|
-
programsService: new ProgramsService(),
|
|
259
|
-
reservedCommunityTypeService: new ReservedCommunityTypesService(),
|
|
260
|
-
unitPriorityService: new UnitAccessibilityPriorityTypesService(),
|
|
261
|
-
unitTypesService: new UnitTypesService(),
|
|
262
|
-
loading: state.loading,
|
|
263
|
-
accessToken: state.accessToken,
|
|
264
|
-
initialStateLoaded: state.initialStateLoaded,
|
|
265
|
-
profile: state.profile,
|
|
266
|
-
loadProfile,
|
|
267
|
-
login: async (
|
|
268
|
-
email,
|
|
269
|
-
password,
|
|
270
|
-
mfaCode: string | undefined = undefined,
|
|
271
|
-
mfaType: EnumLoginMfaType | undefined = undefined
|
|
272
|
-
) => {
|
|
273
|
-
dispatch(signOut())
|
|
274
|
-
dispatch(startLoading())
|
|
275
|
-
try {
|
|
276
|
-
const response = await authService?.login({ body: { email, password, mfaCode, mfaType } })
|
|
277
|
-
if (response) {
|
|
278
|
-
dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch }))
|
|
279
|
-
const profile = await userService?.userControllerProfile()
|
|
280
|
-
if (profile) {
|
|
281
|
-
dispatch(saveProfile(profile))
|
|
282
|
-
return profile
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return undefined
|
|
286
|
-
} finally {
|
|
287
|
-
dispatch(stopLoading())
|
|
288
|
-
}
|
|
289
|
-
},
|
|
290
|
-
loginWithToken: async (token: string) => {
|
|
291
|
-
dispatch(saveToken({ accessToken: token, apiUrl, dispatch }))
|
|
292
|
-
const profile = await userService?.userControllerProfile()
|
|
293
|
-
if (profile) {
|
|
294
|
-
dispatch(saveProfile(profile))
|
|
295
|
-
return profile
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return undefined
|
|
299
|
-
},
|
|
300
|
-
signOut: () => dispatch(signOut()),
|
|
301
|
-
resetPassword: async (token, password, passwordConfirmation) => {
|
|
302
|
-
dispatch(startLoading())
|
|
303
|
-
try {
|
|
304
|
-
const response = await userService?.updatePassword({
|
|
305
|
-
body: {
|
|
306
|
-
token,
|
|
307
|
-
password,
|
|
308
|
-
passwordConfirmation,
|
|
309
|
-
},
|
|
310
|
-
})
|
|
311
|
-
if (response) {
|
|
312
|
-
dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch }))
|
|
313
|
-
const profile = await userService?.userControllerProfile()
|
|
314
|
-
if (profile) {
|
|
315
|
-
dispatch(saveProfile(profile))
|
|
316
|
-
return profile
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return undefined
|
|
320
|
-
} finally {
|
|
321
|
-
dispatch(stopLoading())
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
confirmAccount: async (token) => {
|
|
325
|
-
dispatch(startLoading())
|
|
326
|
-
try {
|
|
327
|
-
const response = await userService?.confirm({ body: { token } })
|
|
328
|
-
if (response) {
|
|
329
|
-
dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch }))
|
|
330
|
-
const profile = await userService?.userControllerProfile()
|
|
331
|
-
if (profile) {
|
|
332
|
-
dispatch(saveProfile(profile))
|
|
333
|
-
return profile
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return undefined
|
|
337
|
-
} finally {
|
|
338
|
-
dispatch(stopLoading())
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
createUser: async (user: UserCreate) => {
|
|
342
|
-
dispatch(startLoading())
|
|
343
|
-
try {
|
|
344
|
-
const response = await userService?.create({
|
|
345
|
-
body: { ...user, appUrl: window.location.origin },
|
|
346
|
-
})
|
|
347
|
-
return response
|
|
348
|
-
} finally {
|
|
349
|
-
dispatch(stopLoading())
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
resendConfirmation: async (email: string) => {
|
|
353
|
-
dispatch(startLoading())
|
|
354
|
-
try {
|
|
355
|
-
const response = await userService?.resendConfirmation({
|
|
356
|
-
body: { email, appUrl: window.location.origin },
|
|
357
|
-
})
|
|
358
|
-
return response
|
|
359
|
-
} finally {
|
|
360
|
-
dispatch(stopLoading())
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
forgotPassword: async (email) => {
|
|
364
|
-
dispatch(startLoading())
|
|
365
|
-
try {
|
|
366
|
-
const response = await userService?.forgotPassword({
|
|
367
|
-
body: { email, appUrl: window.location.origin },
|
|
368
|
-
})
|
|
369
|
-
return response?.message
|
|
370
|
-
} finally {
|
|
371
|
-
dispatch(stopLoading())
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
requestMfaCode: async (email, password, mfaType, phoneNumber) => {
|
|
375
|
-
dispatch(startLoading())
|
|
376
|
-
try {
|
|
377
|
-
return await authService?.requestMfaCode({
|
|
378
|
-
body: { email, password, mfaType, phoneNumber },
|
|
379
|
-
})
|
|
380
|
-
} finally {
|
|
381
|
-
dispatch(stopLoading())
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
}
|
|
385
|
-
return createElement(AuthContext.Provider, { value: contextValues }, children)
|
|
386
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import React, { FunctionComponent, useContext, useEffect, useState } from "react"
|
|
2
|
-
import { clearSiteAlertMessage, setSiteAlertMessage } from "../notifications/SiteAlert"
|
|
3
|
-
import { NavigationContext } from "../config/NavigationContext"
|
|
4
|
-
import { AuthContext } from "./AuthContext"
|
|
5
|
-
|
|
6
|
-
// See https://github.com/Microsoft/TypeScript/issues/14094
|
|
7
|
-
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
|
|
8
|
-
type XOR<T, U> = T | U extends Record<string, unknown>
|
|
9
|
-
? (Without<T, U> & U) | (Without<U, T> & T)
|
|
10
|
-
: T | U
|
|
11
|
-
|
|
12
|
-
type RequireLoginProps = {
|
|
13
|
-
signInPath: string
|
|
14
|
-
signInMessage: string
|
|
15
|
-
termsPath?: string // partners portal required accepted terms after sign-in
|
|
16
|
-
} & XOR<{ requireForRoutes?: string[] }, { skipForRoutes: string[] }>
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Require a login to render children. Will redirect to `signInPath` if not logged in.
|
|
20
|
-
*
|
|
21
|
-
* Props can be specified with either a "whitelist" (list of routes to skip check for) or a "blacklist" (list of
|
|
22
|
-
* routes to apply test on). If no list of routes is provided, then will always apply check.
|
|
23
|
-
*/
|
|
24
|
-
const RequireLogin: FunctionComponent<RequireLoginProps> = ({
|
|
25
|
-
children,
|
|
26
|
-
signInPath,
|
|
27
|
-
signInMessage,
|
|
28
|
-
termsPath,
|
|
29
|
-
...rest
|
|
30
|
-
}) => {
|
|
31
|
-
const { router } = useContext(NavigationContext)
|
|
32
|
-
const { profile, initialStateLoaded } = useContext(AuthContext)
|
|
33
|
-
const [hasTerms, setHasTerms] = useState(false)
|
|
34
|
-
|
|
35
|
-
// Parse just the pathname portion of the signInPath (in case we want to pass URL params)
|
|
36
|
-
const [signInPathname] = signInPath.split("?")
|
|
37
|
-
|
|
38
|
-
// Check if this route requires a login or not (can be specified as a whitelist or a blacklist).
|
|
39
|
-
const loginRequiredForPath =
|
|
40
|
-
// by definition, we shouldn't require login on the sign in page itself
|
|
41
|
-
router.pathname !== signInPathname &&
|
|
42
|
-
("requireForRoutes" in rest
|
|
43
|
-
? rest.requireForRoutes
|
|
44
|
-
? rest.requireForRoutes.some((path) => new RegExp(path).exec(router.pathname))
|
|
45
|
-
: true
|
|
46
|
-
: rest.skipForRoutes
|
|
47
|
-
? !rest.skipForRoutes.some((path) => new RegExp(path).exec(router.pathname))
|
|
48
|
-
: true)
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (profile?.jurisdictions?.some((jurisdiction) => jurisdiction.partnerTerms)) {
|
|
52
|
-
setHasTerms(true)
|
|
53
|
-
}
|
|
54
|
-
}, [profile])
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (loginRequiredForPath && initialStateLoaded && !profile) {
|
|
58
|
-
setSiteAlertMessage(signInMessage, "notice")
|
|
59
|
-
void router.push(signInPath)
|
|
60
|
-
} else {
|
|
61
|
-
clearSiteAlertMessage("notice")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (termsPath && profile && !profile?.agreedToTermsOfService && hasTerms) {
|
|
65
|
-
void router.push(termsPath)
|
|
66
|
-
}
|
|
67
|
-
}, [
|
|
68
|
-
loginRequiredForPath,
|
|
69
|
-
initialStateLoaded,
|
|
70
|
-
profile,
|
|
71
|
-
router,
|
|
72
|
-
signInPath,
|
|
73
|
-
signInMessage,
|
|
74
|
-
termsPath,
|
|
75
|
-
hasTerms,
|
|
76
|
-
])
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
loginRequiredForPath &&
|
|
80
|
-
(!profile || (hasTerms && termsPath && !profile.agreedToTermsOfService))
|
|
81
|
-
) {
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Login either isn't required, or the user object is loaded successfully, continue rendering as normal.
|
|
86
|
-
return <>{children}</>
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export { RequireLogin as default, RequireLogin }
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { AuthContext, AuthProvider } from "./AuthContext"
|
|
2
|
-
export { RequireLogin } from "./RequireLogin"
|
|
3
|
-
export { useRequireLoggedInUser } from "./useRequireLoggedInUser"
|
|
4
|
-
export { ACCESS_TOKEN_LOCAL_STORAGE_KEY } from "./token"
|
|
5
|
-
export { IdleTimeout, LoggedInUserIdleTimeout } from "./timeout"
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import React, { createElement, FunctionComponent, useContext, useEffect, useState } from "react"
|
|
2
|
-
import { AuthContext } from "./AuthContext"
|
|
3
|
-
import { ConfigContext, NavigationContext } from "../config"
|
|
4
|
-
import { Button } from "../actions/Button"
|
|
5
|
-
import { Modal } from "../overlays/Modal"
|
|
6
|
-
import { setSiteAlertMessage } from "../notifications/SiteAlert"
|
|
7
|
-
import { AlertTypes } from "../notifications/alertTypes"
|
|
8
|
-
import { t } from "../helpers/translator"
|
|
9
|
-
import { AppearanceStyleType } from "../global/AppearanceTypes"
|
|
10
|
-
|
|
11
|
-
const PROMPT_TIMEOUT = 60000
|
|
12
|
-
const events = ["mousemove", "keypress", "scroll"]
|
|
13
|
-
|
|
14
|
-
function useIdleTimeout(timeoutMs: number, onTimeout: () => void) {
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
let timer: number
|
|
17
|
-
const restartTimer = () => {
|
|
18
|
-
if (timer) {
|
|
19
|
-
clearTimeout(timer)
|
|
20
|
-
}
|
|
21
|
-
timer = (setTimeout(onTimeout, timeoutMs) as unknown) as number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Listen for any activity events & reset the timer when they are found
|
|
25
|
-
if (typeof document !== "undefined") {
|
|
26
|
-
events.forEach((event) => document.addEventListener(event, restartTimer, false))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Clean up our listeners & clear the timeout on unmounting/updating the effect
|
|
30
|
-
return () => {
|
|
31
|
-
if (timer) {
|
|
32
|
-
clearTimeout(timer)
|
|
33
|
-
}
|
|
34
|
-
events.forEach((event) => document.removeEventListener(event, restartTimer, false))
|
|
35
|
-
}
|
|
36
|
-
}, [timeoutMs, onTimeout])
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type IdleTimeoutProps = {
|
|
40
|
-
promptTitle: string
|
|
41
|
-
promptText: string
|
|
42
|
-
promptAction: string
|
|
43
|
-
redirectPath: string
|
|
44
|
-
alertMessage: string
|
|
45
|
-
alertType?: AlertTypes
|
|
46
|
-
onTimeout: () => unknown
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const IdleTimeout: FunctionComponent<IdleTimeoutProps> = ({
|
|
50
|
-
promptTitle,
|
|
51
|
-
promptAction,
|
|
52
|
-
promptText,
|
|
53
|
-
redirectPath,
|
|
54
|
-
alertMessage,
|
|
55
|
-
alertType = "alert",
|
|
56
|
-
onTimeout,
|
|
57
|
-
}) => {
|
|
58
|
-
const { idleTimeout } = useContext(ConfigContext)
|
|
59
|
-
const [promptTimeout, setPromptTimeout] = useState<number | undefined>()
|
|
60
|
-
const { router } = useContext(NavigationContext)
|
|
61
|
-
|
|
62
|
-
useIdleTimeout(idleTimeout, () => {
|
|
63
|
-
// Clear any existing prompt timeouts
|
|
64
|
-
if (promptTimeout) {
|
|
65
|
-
clearTimeout(promptTimeout)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Give the user 1 minute to respond to the prompt before the onTimeout action
|
|
69
|
-
setPromptTimeout(
|
|
70
|
-
(setTimeout(() => {
|
|
71
|
-
const timeoutAction = async () => {
|
|
72
|
-
setPromptTimeout(undefined)
|
|
73
|
-
await onTimeout()
|
|
74
|
-
setSiteAlertMessage(alertMessage, alertType)
|
|
75
|
-
void router.push(redirectPath)
|
|
76
|
-
}
|
|
77
|
-
void timeoutAction()
|
|
78
|
-
}, PROMPT_TIMEOUT) as unknown) as number
|
|
79
|
-
)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const modalActions = [
|
|
83
|
-
<Button
|
|
84
|
-
styleType={AppearanceStyleType.primary}
|
|
85
|
-
onClick={() => {
|
|
86
|
-
clearTimeout(promptTimeout)
|
|
87
|
-
setPromptTimeout(undefined)
|
|
88
|
-
}}
|
|
89
|
-
>
|
|
90
|
-
{promptAction}
|
|
91
|
-
</Button>,
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<Modal
|
|
96
|
-
open={Boolean(promptTimeout)}
|
|
97
|
-
title={promptTitle}
|
|
98
|
-
ariaDescription={promptText}
|
|
99
|
-
actions={modalActions}
|
|
100
|
-
hideCloseIcon
|
|
101
|
-
role="alertdialog"
|
|
102
|
-
>
|
|
103
|
-
{promptText}
|
|
104
|
-
</Modal>
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const LoggedInUserIdleTimeout = ({ onTimeout }: { onTimeout?: () => unknown }) => {
|
|
109
|
-
const { profile, signOut } = useContext(AuthContext)
|
|
110
|
-
|
|
111
|
-
const timeoutFxn = async () => {
|
|
112
|
-
onTimeout && (await onTimeout())
|
|
113
|
-
signOut && signOut()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Only render the IdleTimeout component if the user is logged in
|
|
117
|
-
return profile && signOut
|
|
118
|
-
? createElement(IdleTimeout, {
|
|
119
|
-
promptTitle: t("t.areYouStillWorking"),
|
|
120
|
-
promptText: t("authentication.timeout.text"),
|
|
121
|
-
promptAction: t("authentication.timeout.action"),
|
|
122
|
-
redirectPath: `/sign-in`,
|
|
123
|
-
alertMessage: t("authentication.timeout.signOutMessage"),
|
|
124
|
-
alertType: "notice",
|
|
125
|
-
onTimeout: timeoutFxn,
|
|
126
|
-
})
|
|
127
|
-
: null
|
|
128
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import jwtDecode from "jwt-decode"
|
|
2
|
-
|
|
3
|
-
export const ACCESS_TOKEN_LOCAL_STORAGE_KEY = "@bht"
|
|
4
|
-
|
|
5
|
-
const getStorage = (type: string) => (type === "local" ? localStorage : sessionStorage)
|
|
6
|
-
|
|
7
|
-
export const setToken = (storageType: string, token: string) =>
|
|
8
|
-
getStorage(storageType).setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, token)
|
|
9
|
-
export const getToken = (storageType: string) =>
|
|
10
|
-
getStorage(storageType).getItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY)
|
|
11
|
-
export const clearToken = (storageType: string) =>
|
|
12
|
-
getStorage(storageType).removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY)
|
|
13
|
-
|
|
14
|
-
export const getTokenTtl = (token: string) => {
|
|
15
|
-
const { exp = 0 } = jwtDecode(token)
|
|
16
|
-
return new Date(exp * 1000).valueOf() - new Date().valueOf()
|
|
17
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useContext } from "react"
|
|
2
|
-
import { AuthContext } from "./AuthContext"
|
|
3
|
-
import { NavigationContext } from "../config/NavigationContext"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Require a logged in user. Waits on initial load, then initiates a redirect to `redirectPath` if user is not
|
|
7
|
-
* logged in.
|
|
8
|
-
*/
|
|
9
|
-
function useRequireLoggedInUser(redirectPath: string) {
|
|
10
|
-
const { profile, initialStateLoaded } = useContext(AuthContext)
|
|
11
|
-
const { router } = useContext(NavigationContext)
|
|
12
|
-
|
|
13
|
-
if (initialStateLoaded && !profile) {
|
|
14
|
-
void router.push(redirectPath)
|
|
15
|
-
}
|
|
16
|
-
return profile
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { useRequireLoggedInUser as default, useRequireLoggedInUser }
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createContext, createElement, FunctionComponent } from "react"
|
|
2
|
-
|
|
3
|
-
type ConfigContextProps = {
|
|
4
|
-
storageType: "local" | "session"
|
|
5
|
-
apiUrl: string
|
|
6
|
-
idleTimeout: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const timeoutMinutes = parseInt(process.env.idleTimeout || process.env.IDLE_TIMEOUT || "5")
|
|
10
|
-
const defaultTimeout = timeoutMinutes * 60 * 1000
|
|
11
|
-
|
|
12
|
-
export const ConfigContext = createContext<ConfigContextProps>({
|
|
13
|
-
storageType: "session",
|
|
14
|
-
apiUrl: "",
|
|
15
|
-
idleTimeout: defaultTimeout,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
export const ConfigProvider: FunctionComponent<{
|
|
19
|
-
apiUrl: string
|
|
20
|
-
storageType?: ConfigContextProps["storageType"]
|
|
21
|
-
idleTimeout?: number
|
|
22
|
-
}> = ({ apiUrl, storageType = "session", idleTimeout = defaultTimeout, children }) => {
|
|
23
|
-
return createElement(
|
|
24
|
-
ConfigContext.Provider,
|
|
25
|
-
{
|
|
26
|
-
value: {
|
|
27
|
-
apiUrl,
|
|
28
|
-
storageType,
|
|
29
|
-
idleTimeout,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
children
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default ConfigContext
|