Skip to content

Commit 3ba16b8

Browse files
authored
[MDS-6294] - User management (#3341)
* Refactor user management and implement user versioning. Removed legacy Redux-based user management in favor of a centralized user mechanism using `userSlice`. Added database and API support for user versioning with history tracking to enable auditability. Updated related frontend components and tests to align with the new structure. * move IUser interface into interfaces directory * remove comment * fixed rebase issue with network reducers * update tests * update tests * add role requirement to user profile resource * added error log to user resource * added error log to user resource * added error log to user resource * remove get_core_users network reducer type * update help test assert * update help test assert * update help test assert
1 parent 98b736e commit 3ba16b8

37 files changed

+689
-436
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
CREATE TABLE "user"
2+
(
3+
sub VARCHAR PRIMARY KEY,
4+
email VARCHAR NOT NULL,
5+
given_name VARCHAR NOT NULL,
6+
family_name VARCHAR NOT NULL,
7+
display_name VARCHAR NOT NULL,
8+
idir_username VARCHAR NOT NULL,
9+
identity_provider VARCHAR NOT NULL,
10+
idir_user_guid VARCHAR NOT NULL,
11+
last_logged_in TIMESTAMPTZ,
12+
create_user VARCHAR(255) NOT NULL,
13+
create_timestamp timestamp with time zone DEFAULT now() NOT NULL,
14+
update_user VARCHAR(255) NOT NULL,
15+
update_timestamp timestamp with time zone DEFAULT now() NOT NULL,
16+
deleted_ind BOOLEAN DEFAULT false
17+
);
18+
19+
ALTER TABLE "user"
20+
OWNER TO mds;
21+
22+
--
23+
-- Name: TABLE user; Type: COMMENT; Schema: public; Owner: mds
24+
--
25+
26+
COMMENT ON TABLE "user" IS 'User Profile data sourced from keycloak';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- This file was generated by the generate_history_table_ddl command
2+
-- The file contains the corresponding history table definition for the {table} table
3+
CREATE TABLE user_version (
4+
create_user VARCHAR(60),
5+
create_timestamp TIMESTAMP WITHOUT TIME ZONE,
6+
update_user VARCHAR(60),
7+
update_timestamp TIMESTAMP WITHOUT TIME ZONE,
8+
deleted_ind BOOLEAN default FALSE,
9+
sub VARCHAR NOT NULL,
10+
email VARCHAR,
11+
given_name VARCHAR,
12+
family_name VARCHAR,
13+
display_name VARCHAR,
14+
idir_username VARCHAR,
15+
identity_provider VARCHAR,
16+
idir_user_guid VARCHAR,
17+
transaction_id BIGINT NOT NULL,
18+
end_transaction_id BIGINT,
19+
operation_type SMALLINT NOT NULL,
20+
PRIMARY KEY (sub, transaction_id)
21+
);
22+
CREATE INDEX ix_user_version_operation_type ON user_version (operation_type);
23+
CREATE INDEX ix_user_version_end_transaction_id ON user_version (end_transaction_id);
24+
CREATE INDEX ix_user_version_transaction_id ON user_version (transaction_id);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- This file was generated by the generate_history_table_ddl command
2+
-- The file contains the data migration to backfill history records for the {table} table
3+
with transaction AS (insert into transaction(id) values(DEFAULT) RETURNING id)
4+
insert into user_version (transaction_id, operation_type, end_transaction_id, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "sub", "email", "given_name", "family_name", "display_name", "idir_username", "identity_provider", "idir_user_guid")
5+
select t.id, '0', null, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "sub", "email", "given_name", "family_name", "display_name", "idir_username", "identity_provider", "idir_user_guid"
6+
from "user",transaction t;

services/common/src/constants/API.ts

+3
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,6 @@ export const REGIONS_LIST = "/regions";
387387
// App Help
388388
export const APP_HELP = (helpKey: string, params?: { system?: string; help_guid?: string }) =>
389389
`/help/${helpKey}?${queryString.stringify(params)}`;
390+
391+
// User
392+
export const USER_PROFILE = () => "/users/profile";

services/common/src/constants/networkReducerTypes.ts

-3
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,6 @@ export enum NetworkReducerTypes {
143143
CREATE_PROJECT_LINKS = "CREATE_PROJECT_LINKS",
144144
DELETE_PROJECT_LINK = "DELETE_PROJECT_LINK",
145145

146-
// Core Users
147-
GET_CORE_USERS = "GET_CORE_USERS",
148-
149146
// Incidents
150147
CREATE_MINE_INCIDENT = "CREATE_MINE_INCIDENT",
151148
GET_INCIDENTS = "GET_INCIDENTS",
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./userInfo.interface";
2+
export * from "./user.interface";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface IUser {
2+
sub: string;
3+
display_name: string;
4+
email: string;
5+
family_name: string;
6+
given_name: string;
7+
last_logged_in: string;
8+
}

services/common/src/redux/actionCreators/userActionCreator.js

-20
This file was deleted.

services/common/src/redux/actions/userActions.js

-8
This file was deleted.

services/common/src/redux/reducers.js

-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import searchReducerObject from "./reducers/searchReducer";
2121
import securitiesReducerObject from "./reducers/securitiesReducer";
2222
import staticContentReducerObject from "./reducers/staticContentReducer";
2323
import tailingsReducerObject from "./reducers/tailingsReducer";
24-
import userReducerObject from "./reducers/userReducer";
2524
import varianceReducerObject from "./reducers/varianceReducer";
2625
import workInformationReducerObject from "./reducers/workInformationReducer";
2726
import verifiableCredentialReducerObject from "./reducers/verifiableCredentialReducer";
@@ -40,7 +39,6 @@ export const permitReducer = permitReducerObject;
4039
export const reportReducer = reportReducerObject;
4140
export const searchReducer = searchReducerObject;
4241
export const staticContentReducer = staticContentReducerObject;
43-
export const userReducer = userReducerObject;
4442
export const varianceReducer = varianceReducerObject;
4543
export const securitiesReducer = securitiesReducerObject;
4644
export const orgbookReducer = orgbookReducerObject;

services/common/src/redux/reducers/rootReducerShared.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
securitiesReducer,
2727
staticContentReducer,
2828
tailingsReducer,
29-
userReducer,
3029
varianceReducer,
3130
verifiableCredentialReducer,
3231
workInformationReducer,
@@ -37,13 +36,17 @@ import regionsReducer from "@mds/common/redux/slices/regionsSlice";
3736
import complianceCodeReducer, { complianceCodeReducerType } from "../slices/complianceCodesSlice";
3837
import spatialDataReducer, { spatialDataReducerType } from "../slices/spatialDataSlice";
3938
import permitServiceReducer, { permitServiceReducerType } from "../slices/permitServiceSlice";
40-
import searchConditionCategoriesReducer, { searchConditionCategoriesType } from "../slices/permitConditionCategorySlice";
39+
import searchConditionCategoriesReducer, {
40+
searchConditionCategoriesType,
41+
} from "../slices/permitConditionCategorySlice";
4142
import helpReducer, { helpReducerType } from "../slices/helpSlice";
4243

4344
const networkReducers = Object.fromEntries(Object.entries(NetworkReducerTypes).map(([key, value]) =>
4445
[NetworkReducerTypes[key], createReducer(networkReducer, value)]
4546
));
4647

48+
import userReducer, { userReducerType } from "@mds/common/redux/slices/userSlice";
49+
4750
export const sharedReducer = {
4851
...activityReducer,
4952
...authenticationReducer,
@@ -67,7 +70,6 @@ export const sharedReducer = {
6770
...securitiesReducer,
6871
...staticContentReducer,
6972
...tailingsReducer,
70-
...userReducer,
7173
...varianceReducer,
7274
...verifiableCredentialReducer,
7375
...workInformationReducer,
@@ -81,5 +83,6 @@ export const sharedReducer = {
8183
[permitServiceReducerType]: permitServiceReducer,
8284
[helpReducerType]: helpReducer,
8385
[searchConditionCategoriesType]: searchConditionCategoriesReducer,
86+
[userReducerType]: userReducer,
8487
...networkReducers
8588
};

services/common/src/redux/reducers/userReducer.js

-26
This file was deleted.

services/common/src/redux/selectors/userSelectors.js

-22
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
import { userReducer, fetchUser, getUser } from "./userSlice"; // Adjust the import path as necessary
3+
import { ENVIRONMENT, USER_PROFILE } from "@mds/common/constants";
4+
import CustomAxios from "@mds/common/redux/customAxios";
5+
6+
const showLoadingMock = jest
7+
.fn()
8+
.mockReturnValue({ type: "SHOW_LOADING", payload: { show: true } });
9+
const hideLoadingMock = jest
10+
.fn()
11+
.mockReturnValue({ type: "HIDE_LOADING", payload: { show: false } });
12+
13+
jest.mock("@mds/common/redux/customAxios");
14+
jest.mock("react-redux-loading-bar", () => ({
15+
showLoading: () => showLoadingMock,
16+
hideLoading: () => hideLoadingMock,
17+
}));
18+
19+
describe("userSlice", () => {
20+
let store;
21+
22+
beforeEach(() => {
23+
store = configureStore({
24+
reducer: {
25+
user: userReducer,
26+
},
27+
});
28+
});
29+
30+
afterEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
describe("fetchUser", () => {
35+
const mockResponse = {
36+
data: {
37+
sub: "mock-sub",
38+
display_name: "Mock User",
39+
40+
family_name: "MockFamily",
41+
given_name: "MockGiven",
42+
last_logged_in: "2023-10-01T12:00:00.000Z",
43+
},
44+
};
45+
46+
it("should fetch user data successfully", async () => {
47+
(CustomAxios as jest.Mock).mockImplementation(() => ({
48+
get: jest.fn().mockResolvedValue(mockResponse),
49+
}));
50+
51+
await store.dispatch(fetchUser());
52+
const state = store.getState().user;
53+
54+
// Verify loading state management
55+
expect(showLoadingMock).toHaveBeenCalledTimes(1);
56+
expect(hideLoadingMock).toHaveBeenCalledTimes(1);
57+
58+
// Verify state update
59+
expect(getUser({ user: state })).toEqual(mockResponse.data);
60+
expect(CustomAxios).toHaveBeenCalledWith({ errorToastMessage: "default" });
61+
});
62+
63+
it("should handle API error", async () => {
64+
const error = new Error("API Error");
65+
(CustomAxios as jest.Mock).mockImplementation(() => ({
66+
get: jest.fn().mockRejectedValue(error),
67+
}));
68+
69+
await store.dispatch(fetchUser());
70+
const state = store.getState().user;
71+
72+
// Check user state remains null on error
73+
expect(getUser({ user: state })).toBeNull();
74+
});
75+
76+
it("should construct the correct endpoint URL", async () => {
77+
const getMock = jest.fn().mockResolvedValue(mockResponse);
78+
(CustomAxios as jest.Mock).mockImplementation(() => ({
79+
get: getMock,
80+
}));
81+
82+
await store.dispatch(fetchUser());
83+
84+
expect(getMock).toHaveBeenCalledWith(
85+
`${ENVIRONMENT.apiUrl}${USER_PROFILE()}`,
86+
expect.any(Object)
87+
);
88+
});
89+
});
90+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createAppSlice, rejectHandler } from "@mds/common/redux/createAppSlice";
2+
import { createRequestHeader } from "@mds/common/redux/utils/RequestHeaders";
3+
import { hideLoading, showLoading } from "react-redux-loading-bar";
4+
import CustomAxios from "@mds/common/redux/customAxios";
5+
import { ENVIRONMENT, USER_PROFILE } from "@mds/common/constants";
6+
import { IUser } from "@mds/common/interfaces";
7+
8+
export const userReducerType = "user";
9+
10+
interface UserState {
11+
user: IUser;
12+
}
13+
14+
const initialState: UserState = {
15+
user: null,
16+
};
17+
18+
const userSlice = createAppSlice({
19+
name: userReducerType,
20+
initialState,
21+
reducers: (create) => ({
22+
fetchUser: create.asyncThunk(
23+
async (_: undefined, thunkApi) => {
24+
const headers = createRequestHeader();
25+
thunkApi.dispatch(showLoading());
26+
27+
const response = await CustomAxios({
28+
errorToastMessage: "default",
29+
}).get(`${ENVIRONMENT.apiUrl}${USER_PROFILE()}`, headers);
30+
31+
thunkApi.dispatch(hideLoading());
32+
return response.data;
33+
},
34+
{
35+
fulfilled: (state: UserState, action) => {
36+
state.user = action.payload;
37+
},
38+
rejected: (state: UserState, action) => {
39+
rejectHandler(action);
40+
},
41+
}
42+
),
43+
}),
44+
selectors: {
45+
getUser: (state) => state.user,
46+
},
47+
});
48+
49+
export const { getUser } = userSlice.selectors;
50+
export const { fetchUser } = userSlice.actions;
51+
export const userReducer = userSlice.reducer;
52+
53+
export default userReducer;

services/common/src/tests/mocks/dataMocks.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
VC_CONNECTION_STATES,
2222
VC_CRED_ISSUE_STATES,
2323
} from "@mds/common/constants";
24-
import { PermitExtraction } from "@mds/common/redux/slices/permitServiceSlice";
2524

2625
export const createMockHeader = () => ({
2726
headers: {
@@ -8967,3 +8966,12 @@ export const HELP_GUIDE_MS = {
89678966
},
89688967
],
89698968
};
8969+
8970+
export const USER = {
8971+
sub: '1234',
8972+
displayName: 'Testerson, Test EMLI:EX',
8973+
8974+
family_name: 'Testerson',
8975+
given_name: 'Test',
8976+
last_logged_in: '2022-08-08T20:59:01.482461+00:00',
8977+
}

0 commit comments

Comments
 (0)