Skip to content

Commit b26e30f

Browse files
Merge pull request #458 from bcgov/BCHEP-589
fix(BCHEP-589): extended logout flow to incl contractors, hardened API auth logic
2 parents a7fe091 + ae2ecb8 commit b26e30f

File tree

3 files changed

+88
-28
lines changed

3 files changed

+88
-28
lines changed

app/frontend/components/domains/navigation/index.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,32 @@ const AppRoutes = observer(() => {
414414

415415
useEffect(() => {
416416
if (tokenExpired) {
417+
const isContractorFlow =
418+
entryPoint === 'isContractor' ||
419+
currentUser?.isContractor ||
420+
sessionStorage.getItem('isContractorFlow') === 'true' ||
421+
location.pathname.startsWith('/contractor');
422+
423+
const isAdminFlow =
424+
entryPoint === 'isAdmin' ||
425+
entryPoint === 'isAdminMgr' ||
426+
entryPoint === 'isSysAdmin' ||
427+
currentUser?.isAdmin ||
428+
currentUser?.isAdminManager ||
429+
currentUser?.isSystemAdmin;
430+
417431
resetAuth();
418432
setAfterLoginPath(location.pathname);
419-
if (entryPoint) {
433+
if (isContractorFlow) {
434+
navigate('/contractor');
435+
} else if (isAdminFlow) {
420436
navigate('/admin');
421437
} else {
422438
navigate('/login');
423439
}
424440
uiStore.flashMessage.show(EFlashMessageStatus.warning, t('auth.tokenExpired'), null);
425441
}
426-
}, [tokenExpired]);
442+
}, [tokenExpired, entryPoint, currentUser, location.pathname, navigate, resetAuth, setAfterLoginPath, t, uiStore]);
427443

428444
useEffect(() => {
429445
const storedPath = sessionStorage.getItem('afterLoginPath') || afterLoginPath;
Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,79 @@
1-
import * as R from "ramda"
2-
import { ISessionStore } from "../../stores/session-store"
3-
import { IUIStore } from "../../stores/ui-store"
4-
import { isNilOrEmpty } from "../../utils"
5-
import { API_ERROR_TYPES } from "../../utils/api-errors"
6-
import { Api } from "."
1+
import * as R from 'ramda';
2+
import { ISessionStore } from '../../stores/session-store';
3+
import { IUIStore } from '../../stores/ui-store';
4+
import { isNilOrEmpty } from '../../utils';
5+
import { API_ERROR_TYPES } from '../../utils/api-errors';
6+
import { Api } from '.';
77

88
/**
99
Watches for "flash" messages from the server and reports them.
1010
*/
1111
export const addFlashMessageMonitor = (api: Api, uiStore: IUIStore) => {
1212
api.addMonitor((response) => {
1313
// assuming these are just CLIENT_ERROR - other types handled below in addApiErrorMonitor
14-
const messageConfig: { [key: string]: any } = R.path(["data", "meta", "message"], response)
15-
if (isNilOrEmpty(messageConfig)) return
14+
const messageConfig: { [key: string]: any } = R.path(['data', 'meta', 'message'], response);
15+
if (isNilOrEmpty(messageConfig)) return;
1616

17-
const { title, message, type } = messageConfig
18-
uiStore.flashMessage.show(type, title, message)
19-
})
20-
}
17+
const { title, message, type } = messageConfig;
18+
uiStore.flashMessage.show(type, title, message);
19+
});
20+
};
2121

2222
export const addApiErrorMonitor = (api: Api, uiStore: IUIStore) => {
2323
api.addMonitor((response) => {
2424
if (response.problem) {
25-
const err = API_ERROR_TYPES[response.problem]
25+
const err = API_ERROR_TYPES[response.problem];
2626
if (err) {
27-
uiStore.flashMessage.show(err.type, err.message, null)
27+
uiStore.flashMessage.show(err.type, err.message, null);
2828
}
2929
}
30-
})
31-
}
30+
});
31+
};
3232

3333
export const addApiUnauthorizedError = (api: Api, sessionStore: ISessionStore) => {
3434
api.addMonitor((response) => {
35-
if (response.status == 401) {
36-
const requestUrl = new URL(response.originalError.request.responseURL)
37-
const whitelistedPaths = ["validate_token", "login"] // avoid infinite loops
35+
if (response.status === 401) {
36+
const requestUrl = response.originalError?.request?.responseURL || response.config?.url || '';
37+
let requestPathname = '';
3838

39-
if (requestUrl && !whitelistedPaths.map((path) => `/api/${path}`).includes(requestUrl.pathname)) {
40-
sessionStore.setTokenExpired(true)
41-
// the redirect logic handled in navigation/index.tsx based on above flag
39+
try {
40+
requestPathname = requestUrl ? new URL(requestUrl, window.location.origin).pathname : '';
41+
} catch {
42+
requestPathname = '';
43+
}
44+
45+
const validateTokenPath = '/api/validate_token';
46+
const loginPath = '/api/login';
47+
const isValidateTokenRequest = requestPathname === validateTokenPath;
48+
const isLoginRequest = requestPathname === loginPath;
49+
50+
// Ignore login 401s and initial unauthenticated validate_token checks.
51+
// If validate_token starts returning 401 after being logged in, treat it as session expiry.
52+
if (isLoginRequest) return;
53+
if (isValidateTokenRequest && !sessionStore.loggedIn) return;
54+
55+
sessionStore.setTokenExpired(true);
56+
// redirect logic handled in navigation/index.tsx based on above flag
57+
58+
const currentUser = sessionStore.rootStore?.userStore?.currentUser;
59+
const isContractorFlow =
60+
sessionStore.entryPoint === 'isContractor' ||
61+
currentUser?.isContractor ||
62+
sessionStorage.getItem('isContractorFlow') === 'true' ||
63+
window.location.pathname.startsWith('/contractor');
64+
const isAdminFlow =
65+
sessionStore.entryPoint === 'isAdmin' ||
66+
sessionStore.entryPoint === 'isAdminMgr' ||
67+
sessionStore.entryPoint === 'isSysAdmin' ||
68+
currentUser?.isAdmin ||
69+
currentUser?.isAdminManager ||
70+
currentUser?.isSystemAdmin;
71+
72+
const targetPath = isContractorFlow ? '/contractor' : isAdminFlow ? '/admin' : '/login';
73+
74+
if (window.location.pathname !== targetPath) {
75+
window.location.replace(targetPath);
4276
}
4377
}
44-
})
45-
}
78+
});
79+
};

app/frontend/stores/session-store.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,24 @@ export const SessionStoreModel = types
6363
logout: flow(function* () {
6464
self.isLoggingOut = true;
6565
const response: any = yield self.environment.api.logout();
66+
67+
const isContractorFlow =
68+
self.entryPoint === 'isContractor' ||
69+
self.rootStore.userStore.currentUser?.isContractor ||
70+
sessionStorage.getItem('isContractorFlow') === 'true' ||
71+
window.location.pathname.startsWith('/contractor');
72+
73+
const isAdminFlow = self.entryPoint === 'isAdmin';
74+
6675
if (response.ok) {
6776
self.resetAuth();
6877
}
78+
6979
const origin = window.location.origin;
7080
let finalRedirect = origin;
71-
if (self.entryPoint === 'isAdmin') {
81+
if (isAdminFlow) {
7282
finalRedirect += '/admin';
73-
} else if (self.entryPoint === 'isContractor') {
83+
} else if (isContractorFlow) {
7484
finalRedirect += '/welcome/contractor';
7585
} else {
7686
finalRedirect += '/';

0 commit comments

Comments
 (0)