Skip to content

Commit

Permalink
[MDS-6294] - User management (#3341)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
matbusby-fw authored Dec 13, 2024
1 parent 98b736e commit 3ba16b8
Show file tree
Hide file tree
Showing 37 changed files with 689 additions and 436 deletions.
26 changes: 26 additions & 0 deletions migrations/sql/V2024.12.10.15.15__create_users_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE TABLE "user"
(
sub VARCHAR PRIMARY KEY,
email VARCHAR NOT NULL,
given_name VARCHAR NOT NULL,
family_name VARCHAR NOT NULL,
display_name VARCHAR NOT NULL,
idir_username VARCHAR NOT NULL,
identity_provider VARCHAR NOT NULL,
idir_user_guid VARCHAR NOT NULL,
last_logged_in TIMESTAMPTZ,
create_user VARCHAR(255) NOT NULL,
create_timestamp timestamp with time zone DEFAULT now() NOT NULL,
update_user VARCHAR(255) NOT NULL,
update_timestamp timestamp with time zone DEFAULT now() NOT NULL,
deleted_ind BOOLEAN DEFAULT false
);

ALTER TABLE "user"
OWNER TO mds;

--
-- Name: TABLE user; Type: COMMENT; Schema: public; Owner: mds
--

COMMENT ON TABLE "user" IS 'User Profile data sourced from keycloak';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the corresponding history table definition for the {table} table
CREATE TABLE user_version (
create_user VARCHAR(60),
create_timestamp TIMESTAMP WITHOUT TIME ZONE,
update_user VARCHAR(60),
update_timestamp TIMESTAMP WITHOUT TIME ZONE,
deleted_ind BOOLEAN default FALSE,
sub VARCHAR NOT NULL,
email VARCHAR,
given_name VARCHAR,
family_name VARCHAR,
display_name VARCHAR,
idir_username VARCHAR,
identity_provider VARCHAR,
idir_user_guid VARCHAR,
transaction_id BIGINT NOT NULL,
end_transaction_id BIGINT,
operation_type SMALLINT NOT NULL,
PRIMARY KEY (sub, transaction_id)
);
CREATE INDEX ix_user_version_operation_type ON user_version (operation_type);
CREATE INDEX ix_user_version_end_transaction_id ON user_version (end_transaction_id);
CREATE INDEX ix_user_version_transaction_id ON user_version (transaction_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the data migration to backfill history records for the {table} table
with transaction AS (insert into transaction(id) values(DEFAULT) RETURNING id)
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")
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"
from "user",transaction t;
3 changes: 3 additions & 0 deletions services/common/src/constants/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,6 @@ export const REGIONS_LIST = "/regions";
// App Help
export const APP_HELP = (helpKey: string, params?: { system?: string; help_guid?: string }) =>
`/help/${helpKey}?${queryString.stringify(params)}`;

// User
export const USER_PROFILE = () => "/users/profile";
3 changes: 0 additions & 3 deletions services/common/src/constants/networkReducerTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,6 @@ export enum NetworkReducerTypes {
CREATE_PROJECT_LINKS = "CREATE_PROJECT_LINKS",
DELETE_PROJECT_LINK = "DELETE_PROJECT_LINK",

// Core Users
GET_CORE_USERS = "GET_CORE_USERS",

// Incidents
CREATE_MINE_INCIDENT = "CREATE_MINE_INCIDENT",
GET_INCIDENTS = "GET_INCIDENTS",
Expand Down
1 change: 1 addition & 0 deletions services/common/src/interfaces/user/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./userInfo.interface";
export * from "./user.interface";
8 changes: 8 additions & 0 deletions services/common/src/interfaces/user/user.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface IUser {
sub: string;
display_name: string;
email: string;
family_name: string;
given_name: string;
last_logged_in: string;
}
20 changes: 0 additions & 20 deletions services/common/src/redux/actionCreators/userActionCreator.js

This file was deleted.

8 changes: 0 additions & 8 deletions services/common/src/redux/actions/userActions.js

This file was deleted.

2 changes: 0 additions & 2 deletions services/common/src/redux/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import searchReducerObject from "./reducers/searchReducer";
import securitiesReducerObject from "./reducers/securitiesReducer";
import staticContentReducerObject from "./reducers/staticContentReducer";
import tailingsReducerObject from "./reducers/tailingsReducer";
import userReducerObject from "./reducers/userReducer";
import varianceReducerObject from "./reducers/varianceReducer";
import workInformationReducerObject from "./reducers/workInformationReducer";
import verifiableCredentialReducerObject from "./reducers/verifiableCredentialReducer";
Expand All @@ -40,7 +39,6 @@ export const permitReducer = permitReducerObject;
export const reportReducer = reportReducerObject;
export const searchReducer = searchReducerObject;
export const staticContentReducer = staticContentReducerObject;
export const userReducer = userReducerObject;
export const varianceReducer = varianceReducerObject;
export const securitiesReducer = securitiesReducerObject;
export const orgbookReducer = orgbookReducerObject;
Expand Down
9 changes: 6 additions & 3 deletions services/common/src/redux/reducers/rootReducerShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
securitiesReducer,
staticContentReducer,
tailingsReducer,
userReducer,
varianceReducer,
verifiableCredentialReducer,
workInformationReducer,
Expand All @@ -37,13 +36,17 @@ import regionsReducer from "@mds/common/redux/slices/regionsSlice";
import complianceCodeReducer, { complianceCodeReducerType } from "../slices/complianceCodesSlice";
import spatialDataReducer, { spatialDataReducerType } from "../slices/spatialDataSlice";
import permitServiceReducer, { permitServiceReducerType } from "../slices/permitServiceSlice";
import searchConditionCategoriesReducer, { searchConditionCategoriesType } from "../slices/permitConditionCategorySlice";
import searchConditionCategoriesReducer, {
searchConditionCategoriesType,
} from "../slices/permitConditionCategorySlice";
import helpReducer, { helpReducerType } from "../slices/helpSlice";

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

import userReducer, { userReducerType } from "@mds/common/redux/slices/userSlice";

export const sharedReducer = {
...activityReducer,
...authenticationReducer,
Expand All @@ -67,7 +70,6 @@ export const sharedReducer = {
...securitiesReducer,
...staticContentReducer,
...tailingsReducer,
...userReducer,
...varianceReducer,
...verifiableCredentialReducer,
...workInformationReducer,
Expand All @@ -81,5 +83,6 @@ export const sharedReducer = {
[permitServiceReducerType]: permitServiceReducer,
[helpReducerType]: helpReducer,
[searchConditionCategoriesType]: searchConditionCategoriesReducer,
[userReducerType]: userReducer,
...networkReducers
};
26 changes: 0 additions & 26 deletions services/common/src/redux/reducers/userReducer.js

This file was deleted.

22 changes: 0 additions & 22 deletions services/common/src/redux/selectors/userSelectors.js

This file was deleted.

90 changes: 90 additions & 0 deletions services/common/src/redux/slices/userSlice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { configureStore } from "@reduxjs/toolkit";
import { userReducer, fetchUser, getUser } from "./userSlice"; // Adjust the import path as necessary
import { ENVIRONMENT, USER_PROFILE } from "@mds/common/constants";
import CustomAxios from "@mds/common/redux/customAxios";

const showLoadingMock = jest
.fn()
.mockReturnValue({ type: "SHOW_LOADING", payload: { show: true } });
const hideLoadingMock = jest
.fn()
.mockReturnValue({ type: "HIDE_LOADING", payload: { show: false } });

jest.mock("@mds/common/redux/customAxios");
jest.mock("react-redux-loading-bar", () => ({
showLoading: () => showLoadingMock,
hideLoading: () => hideLoadingMock,
}));

describe("userSlice", () => {
let store;

beforeEach(() => {
store = configureStore({
reducer: {
user: userReducer,
},
});
});

afterEach(() => {
jest.clearAllMocks();
});

describe("fetchUser", () => {
const mockResponse = {
data: {
sub: "mock-sub",
display_name: "Mock User",
email: "[email protected]",
family_name: "MockFamily",
given_name: "MockGiven",
last_logged_in: "2023-10-01T12:00:00.000Z",
},
};

it("should fetch user data successfully", async () => {
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: jest.fn().mockResolvedValue(mockResponse),
}));

await store.dispatch(fetchUser());
const state = store.getState().user;

// Verify loading state management
expect(showLoadingMock).toHaveBeenCalledTimes(1);
expect(hideLoadingMock).toHaveBeenCalledTimes(1);

// Verify state update
expect(getUser({ user: state })).toEqual(mockResponse.data);
expect(CustomAxios).toHaveBeenCalledWith({ errorToastMessage: "default" });
});

it("should handle API error", async () => {
const error = new Error("API Error");
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: jest.fn().mockRejectedValue(error),
}));

await store.dispatch(fetchUser());
const state = store.getState().user;

// Check user state remains null on error
expect(getUser({ user: state })).toBeNull();
});

it("should construct the correct endpoint URL", async () => {
const getMock = jest.fn().mockResolvedValue(mockResponse);
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: getMock,
}));

await store.dispatch(fetchUser());

expect(getMock).toHaveBeenCalledWith(
`${ENVIRONMENT.apiUrl}${USER_PROFILE()}`,
expect.any(Object)
);
});
});
});
53 changes: 53 additions & 0 deletions services/common/src/redux/slices/userSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createAppSlice, rejectHandler } from "@mds/common/redux/createAppSlice";
import { createRequestHeader } from "@mds/common/redux/utils/RequestHeaders";
import { hideLoading, showLoading } from "react-redux-loading-bar";
import CustomAxios from "@mds/common/redux/customAxios";
import { ENVIRONMENT, USER_PROFILE } from "@mds/common/constants";
import { IUser } from "@mds/common/interfaces";

export const userReducerType = "user";

interface UserState {
user: IUser;
}

const initialState: UserState = {
user: null,
};

const userSlice = createAppSlice({
name: userReducerType,
initialState,
reducers: (create) => ({
fetchUser: create.asyncThunk(
async (_: undefined, thunkApi) => {
const headers = createRequestHeader();
thunkApi.dispatch(showLoading());

const response = await CustomAxios({
errorToastMessage: "default",
}).get(`${ENVIRONMENT.apiUrl}${USER_PROFILE()}`, headers);

thunkApi.dispatch(hideLoading());
return response.data;
},
{
fulfilled: (state: UserState, action) => {
state.user = action.payload;
},
rejected: (state: UserState, action) => {
rejectHandler(action);
},
}
),
}),
selectors: {
getUser: (state) => state.user,
},
});

export const { getUser } = userSlice.selectors;
export const { fetchUser } = userSlice.actions;
export const userReducer = userSlice.reducer;

export default userReducer;
10 changes: 9 additions & 1 deletion services/common/src/tests/mocks/dataMocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
VC_CONNECTION_STATES,
VC_CRED_ISSUE_STATES,
} from "@mds/common/constants";
import { PermitExtraction } from "@mds/common/redux/slices/permitServiceSlice";

export const createMockHeader = () => ({
headers: {
Expand Down Expand Up @@ -8967,3 +8966,12 @@ export const HELP_GUIDE_MS = {
},
],
};

export const USER = {
sub: '1234',
displayName: 'Testerson, Test EMLI:EX',
email: '[email protected]',
family_name: 'Testerson',
given_name: 'Test',
last_logged_in: '2022-08-08T20:59:01.482461+00:00',
}
Loading

0 comments on commit 3ba16b8

Please sign in to comment.