Skip to content

fix(admin-ui): Use typescript generated client for USERS plugin#2338

Merged
duttarnab merged 16 commits intomainfrom
fix-adminui-issues-2337
Oct 27, 2025
Merged

fix(admin-ui): Use typescript generated client for USERS plugin#2338
duttarnab merged 16 commits intomainfrom
fix-adminui-issues-2337

Conversation

@syntrydy
Copy link
Contributor

#2337
closes #2337

@mo-auto mo-auto added comp-admin-ui Component affected by issue or PR kind-bug Issue or PR is a bug in existing functionality labels Oct 13, 2025
@syntrydy syntrydy self-assigned this Oct 15, 2025
@syntrydy syntrydy marked this pull request as ready for review October 15, 2025 12:10
@syntrydy syntrydy requested a review from duttarnab as a code owner October 15, 2025 12:10
Copy link
Contributor

@duttarnab duttarnab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Change password not working.
  • Registered FIDO devices not showing for the users
image - sessions page showing blank - logout not working

@sonarqubecloud
Copy link

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/redux/features/sessionSlice.ts:

@@ -0,0 +1,36 @@
+/**
+ * Session management slice for handling logout audit logging
+ * This is separate from user management to avoid tight coupling
+ */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import reducerRegistry from 'Redux/reducers/ReducerRegistry'
+
+interface SessionState {
+  isUserLogout: boolean
+}
+
+const initialState: SessionState = {
+  isUserLogout: false,
+}
+
+const sessionSlice = createSlice({
+  name: 'session',
+  initialState,
+  reducers: {
+    auditLogoutLogs: (state, action: PayloadAction<{ message: string }>) => {
+      console.log('Logout audit:', action.payload.message)
+      state.isUserLogout = true
+      // This action is handled by saga for actual audit logging
+    },

🧹 Nitpick | 🔵 Trivial

Avoid side-effects in reducers; move logging to saga.

Reducers should be pure. Shift the console.log to the saga handling audit, or guard it behind a dev-only utility.

-    auditLogoutLogs: (state, action: PayloadAction<{ message: string }>) => {
-      console.log('Logout audit:', action.payload.message)
-      state.isUserLogout = true
-      // This action is handled by saga for actual audit logging
-    },
+    auditLogoutLogs: (state, _action: PayloadAction<{ message: string }>) => {
+      state.isUserLogout = true
+      // Actual logging is handled in saga
+    },
🤖 Prompt for AI Agents
In admin-ui/app/redux/features/sessionSlice.ts around lines 20 to 24, the
reducer currently performs a side-effect by calling console.log; remove the
console.log from the reducer to keep it pure and only perform the state mutation
(state.isUserLogout = true). Move the logging to the saga that handles this
auditLogoutLogs action (or wrap it with a dev-only logger utility) so the saga
performs console/logging and any async audit work while the reducer remains
synchronous and side-effect free.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/redux/features/sessionSlice.ts:

@@ -0,0 +1,36 @@
+/**
+ * Session management slice for handling logout audit logging
+ * This is separate from user management to avoid tight coupling
+ */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import reducerRegistry from 'Redux/reducers/ReducerRegistry'
+
+interface SessionState {
+  isUserLogout: boolean
+}
+
+const initialState: SessionState = {
+  isUserLogout: false,
+}
+
+const sessionSlice = createSlice({
+  name: 'session',
+  initialState,
+  reducers: {
+    auditLogoutLogs: (state, action: PayloadAction<{ message: string }>) => {
+      console.log('Logout audit:', action.payload.message)
+      state.isUserLogout = true
+      // This action is handled by saga for actual audit logging
+    },
+    auditLogoutLogsResponse: (state, action: PayloadAction<boolean>) => {
+      state.isUserLogout = action.payload
+    },
+    resetLogoutState: (state) => {
+      state.isUserLogout = false
+    },
+  },

🛠️ Refactor suggestion | 🟠 Major

Clarify state semantics to avoid race/confusion.

isUserLogout flips true before the API result; failures later set it back to false. Prefer explicit flags like logoutAuditInFlight and logoutAuditSucceeded to represent progress vs outcome.

 interface SessionState {
-  isUserLogout: boolean
+  isUserLogout: boolean
+  logoutAuditInFlight: boolean
+  logoutAuditSucceeded: boolean
 }
 
 const initialState: SessionState = {
   isUserLogout: false,
+  logoutAuditInFlight: false,
+  logoutAuditSucceeded: false,
 }
 
 reducers: {
   auditLogoutLogs: (state, _action) => {
-    state.isUserLogout = true
+    state.isUserLogout = true
+    state.logoutAuditInFlight = true
+    state.logoutAuditSucceeded = false
   },
   auditLogoutLogsResponse: (state, action: PayloadAction<boolean>) => {
-    state.isUserLogout = action.payload
+    state.logoutAuditInFlight = false
+    state.logoutAuditSucceeded = action.payload
+    if (!action.payload) {
+      state.isUserLogout = false
+    }
   },
   resetLogoutState: (state) => {
     state.isUserLogout = false
+    state.logoutAuditInFlight = false
+    state.logoutAuditSucceeded = false
   },
 }

Committable suggestion skipped: line range outside the PR's diff.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/redux/sagas/SessionSaga.ts:

@@ -0,0 +1,43 @@
+/**
+ * Session saga for handling logout audit logging
+ */
+import { call, put, takeLatest } from 'redux-saga/effects'
+import type { PayloadAction } from '@reduxjs/toolkit'
+import { auditLogoutLogsResponse } from '../features/sessionSlice'
+import { initAudit } from './SagaUtils'
+import { addAdditionalData } from 'Utils/TokenController'
+import { postUserAction } from 'Redux/api/backend-api'
+import { CREATE } from '../../audit/UserActionType'
+
+const API_USERS = '/api/v1/users'
+
+interface ApiResponse {
+  status?: number
+}
+
+export function* auditLogoutLogsSaga({
+  payload,
+}: PayloadAction<{ message: string }>): Generator<any, boolean, any> {
+  const audit = yield call(initAudit)
+
+  try {
+    addAdditionalData(audit, CREATE, API_USERS, {})
+    audit.message = payload.message
+    const data: ApiResponse = yield call(postUserAction, audit)
+    if (data.status === 200) {
+      yield put(auditLogoutLogsResponse(true))
+    }
+    return true
+  } catch (e: unknown) {
+    yield put(auditLogoutLogsResponse(false))
+    return false
+  }

⚠️ Potential issue | 🔴 Critical

Non-200 response incorrectly treated as success; dispatch failure on all non-2xx.

If postUserAction returns a non-200 (or non-2xx) without throwing, the saga returns true and never updates state to false.

-    const data: ApiResponse = yield call(postUserAction, audit)
-    if (data.status === 200) {
-      yield put(auditLogoutLogsResponse(true))
-    }
-    return true
+    const data: ApiResponse = yield call(postUserAction, audit)
+    const ok = !!data && typeof data.status === 'number' && data.status >= 200 && data.status < 300
+    yield put(auditLogoutLogsResponse(ok))
+    return ok
   } catch (e: unknown) {
     yield put(auditLogoutLogsResponse(false))
     return false
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In admin-ui/app/redux/sagas/SessionSaga.ts around lines 27 to 34, the saga
treats any non-throwing response as success by only checking for status === 200;
change the logic to treat any non-2xx as failure: test the response for a 2xx
status (e.g. response.status >= 200 && response.status < 300 or response.ok) and
only yield put(auditLogoutLogsResponse(true)) and return true when that check
passes, otherwise yield put(auditLogoutLogsResponse(false)) and return false so
non-2xx responses update state as failures.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/redux/sagas/SessionSaga.ts:

@@ -0,0 +1,43 @@
+/**
+ * Session saga for handling logout audit logging
+ */
+import { call, put, takeLatest } from 'redux-saga/effects'
+import type { PayloadAction } from '@reduxjs/toolkit'
+import { auditLogoutLogsResponse } from '../features/sessionSlice'
+import { initAudit } from './SagaUtils'
+import { addAdditionalData } from 'Utils/TokenController'
+import { postUserAction } from 'Redux/api/backend-api'
+import { CREATE } from '../../audit/UserActionType'
+
+const API_USERS = '/api/v1/users'
+
+interface ApiResponse {
+  status?: number
+}
+
+export function* auditLogoutLogsSaga({
+  payload,
+}: PayloadAction<{ message: string }>): Generator<any, boolean, any> {
+  const audit = yield call(initAudit)
+
+  try {
+    addAdditionalData(audit, CREATE, API_USERS, {})
+    audit.message = payload.message
+    const data: ApiResponse = yield call(postUserAction, audit)
+    if (data.status === 200) {
+      yield put(auditLogoutLogsResponse(true))
+    }
+    return true
+  } catch (e: unknown) {
+    yield put(auditLogoutLogsResponse(false))
+    return false
+  }
+}
+
+export function* watchAuditLogoutLogs(): Generator<any, void, any> {
+  yield takeLatest('session/auditLogoutLogs', auditLogoutLogsSaga)
+}

🛠️ Refactor suggestion | 🟠 Major

Avoid stringly-typed action; use the slice action’s type.

Prevents typos and eases refactors.

-import { auditLogoutLogsResponse } from '../features/sessionSlice'
+import { auditLogoutLogs, auditLogoutLogsResponse } from '../features/sessionSlice'

 export function* watchAuditLogoutLogs(): Generator<any, void, any> {
-  yield takeLatest('session/auditLogoutLogs', auditLogoutLogsSaga)
+  yield takeLatest(auditLogoutLogs.type, auditLogoutLogsSaga)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { auditLogoutLogs, auditLogoutLogsResponse } from '../features/sessionSlice'

export function* watchAuditLogoutLogs(): Generator<any, void, any> {
  yield takeLatest(auditLogoutLogs.type, auditLogoutLogsSaga)
}
🤖 Prompt for AI Agents
In admin-ui/app/redux/sagas/SessionSaga.ts around lines 37-39, the watcher uses
a string literal 'session/auditLogoutLogs' which is fragile; import the
auditLogoutLogs action (or the session slice) and replace the string with the
action's type (e.g., auditLogoutLogs.type) when calling takeLatest so the
watcher references the slice action constant rather than a hard-coded string,
ensuring you import the action at the top and call yield
takeLatest(auditLogoutLogs.type, auditLogoutLogsSaga).

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/redux/sagas/SessionSaga.ts:

@@ -0,0 +1,43 @@
+/**
+ * Session saga for handling logout audit logging
+ */
+import { call, put, takeLatest } from 'redux-saga/effects'
+import type { PayloadAction } from '@reduxjs/toolkit'
+import { auditLogoutLogsResponse } from '../features/sessionSlice'
+import { initAudit } from './SagaUtils'
+import { addAdditionalData } from 'Utils/TokenController'
+import { postUserAction } from 'Redux/api/backend-api'
+import { CREATE } from '../../audit/UserActionType'
+
+const API_USERS = '/api/v1/users'
+
+interface ApiResponse {
+  status?: number
+}
+
+export function* auditLogoutLogsSaga({
+  payload,
+}: PayloadAction<{ message: string }>): Generator<any, boolean, any> {
+  const audit = yield call(initAudit)
+
+  try {
+    addAdditionalData(audit, CREATE, API_USERS, {})
+    audit.message = payload.message
+    const data: ApiResponse = yield call(postUserAction, audit)
+    if (data.status === 200) {
+      yield put(auditLogoutLogsResponse(true))
+    }
+    return true
+  } catch (e: unknown) {
+    yield put(auditLogoutLogsResponse(false))
+    return false
+  }
+}
+
+export function* watchAuditLogoutLogs(): Generator<any, void, any> {
+  yield takeLatest('session/auditLogoutLogs', auditLogoutLogsSaga)
+}
+
+export default function* sessionSaga(): Generator<any, void, any> {
+  yield call(watchAuditLogoutLogs)
+}

🧹 Nitpick | 🔵 Trivial

Non-blocking watchers pattern (optional).

If more watchers are added later, switch to fork/all to avoid blocking.

-import default function* sessionSaga(): Generator<any, void, any> {
-  yield call(watchAuditLogoutLogs)
-}
+import { all, fork } from 'redux-saga/effects'
+export default function* sessionSaga(): Generator<any, void, any> {
+  yield all([fork(watchAuditLogoutLogs)])
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In admin-ui/app/redux/sagas/SessionSaga.ts around lines 41 to 43, the saga
currently uses yield call(watchAuditLogoutLogs) which will block the root saga
if additional watchers are added; change to the non-blocking watchers pattern by
importing fork and all from 'redux-saga/effects' and replace the single blocking
call with yield all([fork(watchAuditLogoutLogs)]) so watchers run concurrently
and won't block each other as you add more.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/app/routes/Apps/Profile/ProfilePage.tsx:

@@ -117,10 +116,11 @@ const ProfileDetails: React.FC<ProfileDetailsProps> = () => {
 
   const navigateToUserManagement = useCallback((): void => {
     if (profileDetails) {
-      dispatch(setSelectedUserData(profileDetails))
-      navigate(`/user/usermanagement/edit/:${profileDetails.inum}`)
+      navigate(`/user/usermanagement/edit/:${profileDetails.inum}`, {
+        state: { selectedUser: profileDetails },
+      })
     }
-  }, [profileDetails, dispatch, navigate])
+  }, [profileDetails, navigate])

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Bug: route parameter is passed with a leading colon

In a navigate call, the dynamic segment should be the concrete value without a colon. Current code will likely not match the route.

Apply this diff and guard for missing inum:

-  navigate(`/user/usermanagement/edit/:${profileDetails.inum}`, {
-    state: { selectedUser: profileDetails },
-  })
+  if (!profileDetails.inum) return
+  navigate(`/user/usermanagement/edit/${encodeURIComponent(profileDetails.inum)}`, {
+    state: { selectedUser: profileDetails },
+  })

Also confirm the edit page reads location.state.selectedUser (or falls back to fetching by param).


🏁 Script executed:

#!/bin/bash
# Find consumers reading selectedUser from navigation state and param usage
rg -nP --type=ts --type=tsx -C2 '\bselectedUser\b|useLocation\(|useParams\(' admin-ui/plugins/user-management

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search with corrected file type handling (ts only, or use glob for tsx)
# Look for route definitions and how selectedUser/useParams are used
rg -nP --type=ts -C3 'edit.*:\$|useLocation|useParams|selectedUser' admin-ui/plugins/user-management

# Also search for route definitions in the entire admin-ui
echo "=== Route definitions ==="
rg -nP --type=ts -C2 'user/usermanagement/edit' admin-ui/

# Specifically look at the edit page implementation
echo "=== Edit page file search ==="
fd -e ts -e tsx 'edit' admin-ui/plugins/user-management/ 2>/dev/null | head -20

Length of output: 5247


🏁 Script executed:

#!/bin/bash
# Find route definitions to understand the expected pattern
echo "=== Looking for route definitions ==="
rg -nP --type=ts 'path.*user.*usermanagement.*edit|Route.*user.*edit' admin-ui/ | head -20

# Check if there's a Routes or AppRoutes file
echo "=== Finding Routes configuration ==="
fd -e ts -e tsx 'route|Route' admin-ui/app/ 2>/dev/null | grep -i route | head -10

Length of output: 309


Bug: route parameter is passed with a leading colon in navigate calls

In navigate paths, dynamic segments must be concrete values without colons. The code has : which will prevent route matching. Additionally, the parameter should be URL-encoded for special characters and guarded against missing values.

This same pattern appears in two places:

  • admin-ui/app/routes/Apps/Profile/ProfilePage.tsx:119 (file under review)
  • admin-ui/plugins/user-management/components/UserList.tsx:223

Apply this fix to both locations:

-  navigate(`/user/usermanagement/edit/:${profileDetails.inum}`, {
-    state: { selectedUser: profileDetails },
-  })
+  if (!profileDetails.inum) return
+  navigate(`/user/usermanagement/edit/${encodeURIComponent(profileDetails.inum)}`, {
+    state: { selectedUser: profileDetails },
+  })

The edit page correctly reads the passed state (location.state?.selectedUser), so passing state is the right approach; just fix the path format.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      if (!profileDetails.inum) return
      navigate(`/user/usermanagement/edit/${encodeURIComponent(profileDetails.inum)}`, {
        state: { selectedUser: profileDetails },
      })
    }
  }, [profileDetails, navigate])
🤖 Prompt for AI Agents
admin-ui/app/routes/Apps/Profile/ProfilePage.tsx lines 119-123: the navigate
call incorrectly includes a leading colon in the path and doesn't guard or
encode the dynamic value; replace the path so it uses the concrete, encoded inum
(e.g., .../user/usermanagement/edit/${encodeURIComponent(profileDetails.inum)})
and only call navigate when profileDetails?.inum exists, preserving the state
payload; apply the same fix pattern to
admin-ui/plugins/user-management/components/UserList.tsx at the analogous call.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/__tests__/api/Users.test.js:

@@ -1,107 +1,20 @@
-import { authReducerInit, beforeAllAsync } from 'Plugins/auth-server/__tests__/api/setup.test'
-import { combineReducers } from '@reduxjs/toolkit'
-import authReducer from 'Redux/features/authSlice'
-import { expectSaga } from 'redux-saga-test-plan'
-import {
-  getUsersSaga,
-  createUserSaga,
-  updateUserSaga,
-  deleteUserSaga,
-} from 'Plugins/user-management/redux/sagas/UserSaga'
-import userReducer, {
-  initialState as userInitState,
-} from 'Plugins/user-management/redux/features/userSlice'
-import { log } from 'console'
-
-let initialState
-
-const formInitState = (token, issuer) => {
-  initialState = {
-    authReducer: authReducerInit(token, issuer),
-    userReducer: userInitState,
-  }
-}
-
-beforeAll(async () => {
-  try {
-    await beforeAllAsync(formInitState)
-  } catch (error) {
-    log(error.message)
-  }
-})
-
-const rootReducer = combineReducers({
-  authReducer,
-  userReducer,
-})
-
-const payload = {
-  userId: 'test_modle',
-  mail: 'example@email.com',
-  displayName: 'test display name',
-  status: 'inactive',
-  userPassword: '12345678',
-  givenName: 'test',
-  customAttributes: [
-    {
-      name: 'sn',
-      multiValued: false,
-      values: ['module'],
-    },
-    {
-      name: 'middleName',
-      multiValued: false,
-      values: ['test'],
-    },
-  ],
-}
-
-describe('perform CRUD for user management module', () => {
-  let user
-  it('GET user list', async () => {
-    const result = await expectSaga(getUsersSaga, { payload: {} })
-      .withReducer(rootReducer, initialState)
-      .silentRun(false)
-
-    expect(result.returnValue instanceof Error).toBe(false)
-  })
-
-  it('create new user', async () => {
-    const result = await expectSaga(createUserSaga, { payload: payload })
-      .withReducer(rootReducer, initialState)
-      .silentRun(false)
-
-    if (!(result.returnValue instanceof Error)) {
-      user = result.returnValue
-    }
-    expect(result.returnValue instanceof Error).toBe(false)
-  })
-
-  it('update newly created user', async () => {
-    if (user) {
-      const result = await expectSaga(updateUserSaga, {
-        payload: { ...user, displayName: 'update_test' },
-      })
-        .withReducer(rootReducer, initialState)
-        .silentRun(false)
-
-      expect(result.returnValue instanceof Error).toBe(false)
-    } else {
-      log('skipping test, no ldap config found!')
-    }
-  })
-
-  it('delete newly created test ldap', async () => {
-    if (user) {
-      const result = await expectSaga(deleteUserSaga, {
-        payload: user.inum,
-      })
-        .withReducer(rootReducer, initialState)
-        .silentRun(false)
-
-      expect(result.returnValue instanceof Error).toBe(false)
-    } else {
-      log('skipping test, no ldap config found!')
-    }
+/**
+ * NOTE: These tests have been temporarily disabled as part of the migration
+ * from Redux Sagas to React Query (Orval-generated hooks).
+ *
+ * The user management plugin has been fully migrated to use:
+ * - React Query hooks (useGetUser, usePostUser, usePutUser, useDeleteUser)
+ * - No more Redux sagas or slices for user management
+ *
+ * These tests need to be rewritten to test the React Query hooks directly
+ * using React Testing Library and MSW (Mock Service Worker) instead of
+ * redux-saga-test-plan.
+ *
+ * TODO: Rewrite these tests for React Query
+ */
+
+describe.skip('User Management CRUD Tests (Disabled - Needs Rewrite for React Query)', () => {
+  it('placeholder test', () => {
+    expect(true).toBe(true)
   })
 })

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify test coverage gap is tracked and has timeline.

The test suite has been completely disabled as part of the React Query migration. While the comment clearly explains the context, this leaves user management CRUD operations without test coverage.

Please confirm:

  1. Is there a tracking issue/ticket for rewriting these tests?
  2. What is the timeline for restoring test coverage?
  3. Are the React Query hooks being tested elsewhere in the meantime?

Consider adding the issue/ticket reference to the TODO comment for traceability.


🏁 Script executed:

#!/bin/bash
# Check if there are any other test files for user management that might provide coverage
fd -e test.js -e test.tsx -e spec.js -e spec.tsx . admin-ui/plugins/user-management/ --exec echo "Found test file: {}"

# Search for any React Query hook tests related to user management
rg -n --type=js --type=tsx -C3 'useGetUser|usePostUser|usePutUser|useDeleteUser' 'admin-ui/plugins/user-management/'

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Broader search for React Query hooks - search entire codebase without tsx type constraint
rg -n 'useGetUser|usePostUser|usePutUser|useDeleteUser' --type=js --type=jsx 'admin-ui/'

# Check if there are hook implementation files or generated files (Orval generates hooks)
fd -e js -e ts 'user' 'admin-ui/plugins/user-management/' -path '*/queries*' -o -path '*/hooks*' -o -path '*/generated*'

# Search for any issue/ticket references in files related to user management
rg -n 'TODO|FIXME|ticket|issue|jira' 'admin-ui/plugins/user-management/' -C2

Length of output: 1377


🏁 Script executed:

#!/bin/bash
# Check the actual structure of user-management plugin
find admin-ui/plugins/user-management/ -type f \( -name '*.js' -o -name '*.ts' -o -name '*.jsx' \) | head -20

Length of output: 554


🏁 Script executed:

#!/bin/bash
# Search for Orval-generated files or API hooks (typical patterns)
find admin-ui/ -type d \( -name 'generated' -o -name 'hooks' -o -name 'queries' -o -name 'api' \) 2>/dev/null | head -20

# Search for any files containing the hook imports/exports
rg -n 'useGetUser|usePostUser|usePutUser|useDeleteUser' admin-ui/ 2>/dev/null | head -20

# Look for any other test files in user-management or related plugins
find admin-ui/plugins/user-management/ -type f -name '*.test.*' -o -name '*.spec.*'

# Check if there are any .tsx or .jsx component files that might have tests elsewhere
find admin-ui/plugins/user-management/ -type f \( -name '*.tsx' -o -name '*.jsx' \)

Length of output: 2127


🏁 Script executed:

#!/bin/bash
# Search for any component test files in the admin-ui directory that test user-related functionality
find admin-ui/ -type f \( -name '*.test.tsx' -o -name '*.test.jsx' -o -name '*.test.ts' \) 2>/dev/null

# Look for issue/ticket references in README or docs
find admin-ui/plugins/user-management/ -type f \( -name 'README*' -o -name '*.md' \) -exec cat {} \;

# Check git history for recent migration-related commits (if available)
git log --all --oneline --grep='React Query\|migration' -- admin-ui/plugins/user-management/ 2>/dev/null | head -10

Length of output: 1431


Tests for active React Query hooks are missing; add tracking and restore coverage.

The React Query hooks are already implemented and actively used in components (UserList.tsx, UserAddPage.tsx, UserEditPage.tsx), but there is no test coverage for either the hooks or the components using them. The disabled Users.test.js is the only test file in the plugin, and it has no tracking reference.

Issues:

Required:

  1. Create a tracked issue/ticket for this migration
  2. Add that reference to the TODO comment
  3. Prioritize writing component tests using React Testing Library + MSW for the user management components

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserAddPage.tsx:

@@ -1,93 +1,96 @@
-import React, { useEffect } from 'react'
+import React from 'react'
 import { useNavigate } from 'react-router-dom'
 import { Container, CardBody, Card } from '../../../app/components'
 import UserForm from './UserForm'
 import GluuAlert from '../../../app/routes/Apps/Gluu/GluuAlert'
 import { useTranslation } from 'react-i18next'
-import { createUser } from '../redux/features/userSlice'
 import { useDispatch, useSelector } from 'react-redux'
 import moment from 'moment'
 import {
   PersonAttribute,
   UserManagementRootState,
-  UserModifyOptions,
 } from 'Plugins/user-management/types/UserApiTypes'
 import { UserEditFormValues } from '../types/ComponentTypes'
-import { setSelectedUserData } from '../redux/features/userSlice'
+import { usePostUser, getGetUserQueryKey, CustomUser, CustomObjectAttribute } from 'JansConfigApi'
+import { useQueryClient } from '@tanstack/react-query'
+import { updateToast } from 'Redux/features/toastSlice'
+import { logUserCreation, getErrorMessage } from '../helper/userAuditHelpers'
+import { triggerUserWebhook } from '../helper/userWebhookHelpers'
 
 function UserAddPage() {
   const dispatch = useDispatch()
   const navigate = useNavigate()
-  const redirectToUserListPage = useSelector(
-    (state: UserManagementRootState) => state.userReducer.redirectToUserListPage,
-  )
+  const queryClient = useQueryClient()
   const { t } = useTranslation()
   const personAttributes = useSelector(
     (state: UserManagementRootState) => state.attributesReducerRoot.items,
   )
-  const createCustomAttributes = (values: UserEditFormValues) => {
-    const customAttributes: Array<{
-      name: string
-      multiValued: boolean
-      values: string[]
-    }> = []
-    if (values) {
-      for (const key in values) {
-        const customAttribute = personAttributes.filter((e: PersonAttribute) => e.name == key)
-        if (personAttributes.some((e: PersonAttribute) => e.name == key)) {
-          let obj: {
-            name: string
-            multiValued: boolean
-            values: string[]
-          }
-          if (!customAttribute[0]?.oxMultiValuedAttribute) {
-            const val: string[] = []
-            if (key != 'birthdate') {
-              if (typeof values[key] === 'string') {
-                val.push(values[key] as string)
-              } else if (Array.isArray(values[key])) {
-                val.push(...(values[key] as string[]))
-              }
-            } else {
-              const dateValue = values[key] as string
-              const formattedDate = moment(dateValue, 'YYYY-MM-DD').format('YYYY-MM-DD')
-              val.push(formattedDate)
-            }
-            obj = {
-              name: key,
-              multiValued: false,
-              values: val,
-            }
-          } else {
-            const valE: string[] = []
-            const fieldValue = values[key]
-            if (Array.isArray(fieldValue)) {
-              for (const i in fieldValue) {
-                const item = fieldValue[i]
-                if (typeof item === 'object' && item !== null) {
-                  // Handle object case - extract the key value
-                  const objectItem = item as Record<string, string>
-                  if (objectItem[key]) {
-                    valE.push(objectItem[key])
-                  }
-                } else if (typeof item === 'string') {
-                  valE.push(item)
-                }
-              }
-            } else if (typeof fieldValue === 'string') {
-              valE.push(fieldValue)
-            }
-            obj = {
-              name: key,
-              multiValued: true,
-              values: valE,
-            }
-          }
-          customAttributes.push(obj)
+  const createUserMutation = usePostUser({
+    mutation: {
+      onSuccess: async (data, variables) => {
+        dispatch(updateToast(true, 'success', t('messages.user_created_successfully')))
+        await logUserCreation(data, variables.data)
+        await triggerUserWebhook(data)
+        queryClient.invalidateQueries({ queryKey: getGetUserQueryKey() })
+        navigate('/user/usersmanagement')
+      },
+      onError: (error: unknown) => {
+        const errMsg = getErrorMessage(error)
+        dispatch(updateToast(true, 'error', errMsg))
+      },
+    },
+  })
+  const createCustomAttributes = (values: UserEditFormValues): CustomObjectAttribute[] => {
+    const customAttributes: CustomObjectAttribute[] = []
+    if (!values) {
+      return customAttributes
+    }
+
+    const attributeByName = new Map(
+      personAttributes.map((attr: PersonAttribute) => [attr.name, attr]),
+    )
+    const toStringValue = (key: string, value: unknown): string | undefined => {
+      if (typeof value === 'string') {
+        return value
+      }
+      if (value && typeof value === 'object') {
+        const obj = value as Record<string, unknown>
+        if (typeof obj[key] === 'string') return obj[key]
+        if (typeof obj.value === 'string') return obj.value
+        if (typeof obj.label === 'string') return obj.label
+      }
+      return undefined
+    }
+
+    const normalizeValues = (key: string, rawValue: unknown, multiValued: boolean): string[] => {
+      if (!multiValued && key === 'birthdate' && typeof rawValue === 'string') {
+        const formattedDate = moment(rawValue, 'YYYY-MM-DD').format('YYYY-MM-DD')
+        return [formattedDate]
+      }
+      const items = Array.isArray(rawValue) ? rawValue : [rawValue]
+      const result: string[] = []
+      for (const item of items) {
+        const str = toStringValue(key, item)
+        if (typeof str === 'string' && str.length > 0) {
+          result.push(str)
         }
       }
-      return customAttributes
+      return result
+    }

⚠️ Potential issue | 🟠 Major

Birthdate normalization should be strict and skip invalid dates.

Avoid “Invalid date” strings.

-      if (!multiValued && key === 'birthdate' && typeof rawValue === 'string') {
-        const formattedDate = moment(rawValue, 'YYYY-MM-DD').format('YYYY-MM-DD')
-        return [formattedDate]
-      }
+      if (!multiValued && key === 'birthdate' && typeof rawValue === 'string') {
+        const m = moment(rawValue, 'YYYY-MM-DD', true)
+        return m.isValid() ? [m.format('YYYY-MM-DD')] : []
+      }
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserAddPage.tsx around lines 65
to 79, the birthdate normalization currently can produce the string "Invalid
date"; change the branch that handles !multiValued && key === 'birthdate' &&
typeof rawValue === 'string' to parse the date strictly and skip invalid values:
use strict parsing (moment(rawValue, 'YYYY-MM-DD', true)), check
moment.isValid(), and only format and return the date when valid; if invalid,
return an empty array so no "Invalid date" string is included.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserAddPage.tsx:

@@ -1,93 +1,96 @@
-import React, { useEffect } from 'react'
+import React from 'react'
 import { useNavigate } from 'react-router-dom'
 import { Container, CardBody, Card } from '../../../app/components'
 import UserForm from './UserForm'
 import GluuAlert from '../../../app/routes/Apps/Gluu/GluuAlert'
 import { useTranslation } from 'react-i18next'
-import { createUser } from '../redux/features/userSlice'
 import { useDispatch, useSelector } from 'react-redux'
 import moment from 'moment'
 import {
   PersonAttribute,
   UserManagementRootState,
-  UserModifyOptions,
 } from 'Plugins/user-management/types/UserApiTypes'
 import { UserEditFormValues } from '../types/ComponentTypes'
-import { setSelectedUserData } from '../redux/features/userSlice'
+import { usePostUser, getGetUserQueryKey, CustomUser, CustomObjectAttribute } from 'JansConfigApi'
+import { useQueryClient } from '@tanstack/react-query'
+import { updateToast } from 'Redux/features/toastSlice'
+import { logUserCreation, getErrorMessage } from '../helper/userAuditHelpers'
+import { triggerUserWebhook } from '../helper/userWebhookHelpers'
 
 function UserAddPage() {
   const dispatch = useDispatch()
   const navigate = useNavigate()
-  const redirectToUserListPage = useSelector(
-    (state: UserManagementRootState) => state.userReducer.redirectToUserListPage,
-  )
+  const queryClient = useQueryClient()
   const { t } = useTranslation()
   const personAttributes = useSelector(
     (state: UserManagementRootState) => state.attributesReducerRoot.items,
   )
-  const createCustomAttributes = (values: UserEditFormValues) => {
-    const customAttributes: Array<{
-      name: string
-      multiValued: boolean
-      values: string[]
-    }> = []
-    if (values) {
-      for (const key in values) {
-        const customAttribute = personAttributes.filter((e: PersonAttribute) => e.name == key)
-        if (personAttributes.some((e: PersonAttribute) => e.name == key)) {
-          let obj: {
-            name: string
-            multiValued: boolean
-            values: string[]
-          }
-          if (!customAttribute[0]?.oxMultiValuedAttribute) {
-            const val: string[] = []
-            if (key != 'birthdate') {
-              if (typeof values[key] === 'string') {
-                val.push(values[key] as string)
-              } else if (Array.isArray(values[key])) {
-                val.push(...(values[key] as string[]))
-              }
-            } else {
-              const dateValue = values[key] as string
-              const formattedDate = moment(dateValue, 'YYYY-MM-DD').format('YYYY-MM-DD')
-              val.push(formattedDate)
-            }
-            obj = {
-              name: key,
-              multiValued: false,
-              values: val,
-            }
-          } else {
-            const valE: string[] = []
-            const fieldValue = values[key]
-            if (Array.isArray(fieldValue)) {
-              for (const i in fieldValue) {
-                const item = fieldValue[i]
-                if (typeof item === 'object' && item !== null) {
-                  // Handle object case - extract the key value
-                  const objectItem = item as Record<string, string>
-                  if (objectItem[key]) {
-                    valE.push(objectItem[key])
-                  }
-                } else if (typeof item === 'string') {
-                  valE.push(item)
-                }
-              }
-            } else if (typeof fieldValue === 'string') {
-              valE.push(fieldValue)
-            }
-            obj = {
-              name: key,
-              multiValued: true,
-              values: valE,
-            }
-          }
-          customAttributes.push(obj)
+  const createUserMutation = usePostUser({
+    mutation: {
+      onSuccess: async (data, variables) => {
+        dispatch(updateToast(true, 'success', t('messages.user_created_successfully')))
+        await logUserCreation(data, variables.data)
+        await triggerUserWebhook(data)
+        queryClient.invalidateQueries({ queryKey: getGetUserQueryKey() })
+        navigate('/user/usersmanagement')
+      },
+      onError: (error: unknown) => {
+        const errMsg = getErrorMessage(error)
+        dispatch(updateToast(true, 'error', errMsg))
+      },
+    },
+  })
+  const createCustomAttributes = (values: UserEditFormValues): CustomObjectAttribute[] => {
+    const customAttributes: CustomObjectAttribute[] = []
+    if (!values) {
+      return customAttributes
+    }
+
+    const attributeByName = new Map(
+      personAttributes.map((attr: PersonAttribute) => [attr.name, attr]),
+    )
+    const toStringValue = (key: string, value: unknown): string | undefined => {
+      if (typeof value === 'string') {
+        return value
+      }
+      if (value && typeof value === 'object') {
+        const obj = value as Record<string, unknown>
+        if (typeof obj[key] === 'string') return obj[key]
+        if (typeof obj.value === 'string') return obj.value
+        if (typeof obj.label === 'string') return obj.label
+      }
+      return undefined
+    }
+
+    const normalizeValues = (key: string, rawValue: unknown, multiValued: boolean): string[] => {
+      if (!multiValued && key === 'birthdate' && typeof rawValue === 'string') {
+        const formattedDate = moment(rawValue, 'YYYY-MM-DD').format('YYYY-MM-DD')
+        return [formattedDate]
+      }
+      const items = Array.isArray(rawValue) ? rawValue : [rawValue]
+      const result: string[] = []
+      for (const item of items) {
+        const str = toStringValue(key, item)
+        if (typeof str === 'string' && str.length > 0) {
+          result.push(str)
         }
       }
-      return customAttributes
+      return result
+    }
+
+    for (const [key, raw] of Object.entries(values)) {
+      const attr = attributeByName.get(key)
+      if (!attr) continue
+      const multiValued = !!attr.oxMultiValuedAttribute
+      const valuesArray = normalizeValues(key, raw, multiValued)
+      const obj: CustomObjectAttribute = {
+        name: key,
+        multiValued,
+        values: valuesArray as unknown as CustomObjectAttribute['values'],
+      }
+      customAttributes.push(obj)

🧹 Nitpick | 🔵 Trivial

Type cast on values: avoid double cast if possible.

If SDK types accept string[], remove unknown-as pattern to keep typings clean. Otherwise leave as-is.

-        values: valuesArray as unknown as CustomObjectAttribute['values'],
+        values: valuesArray,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      const obj: CustomObjectAttribute = {
        name: key,
        multiValued,
        values: valuesArray,
      }
      customAttributes.push(obj)
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserAddPage.tsx around lines 86
to 91, the code uses a double cast for values (valuesArray as unknown as
CustomObjectAttribute['values']); if the SDK type for
CustomObjectAttribute['values'] is compatible with string[] then remove the
unnecessary casts and assign values: valuesArray directly; otherwise replace the
double cast with a single, explicit cast values: valuesArray as
CustomObjectAttribute['values'] to keep typings clean and avoid the unknown
intermediary.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserAddPage.tsx:

@@ -96,30 +99,18 @@ function UserAddPage() {
     message: string,
   ) => {
     const customAttributes = createCustomAttributes(values)
-    const submitableValues = {
+    const submitableValues: CustomUser = {
       userId: values.userId || '',
       mail: values.mail,
       displayName: values.displayName || '',
-      status: values.status || '',
-      userPassword: values.userPassword || '',
+      status: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
+      userPassword: values.userPassword as string | undefined,
       givenName: values.givenName || '',
       customAttributes: customAttributes,
-      action_message: message,
     }
-    dispatch(createUser(submitableValues as UserModifyOptions))
+    createUserMutation.mutate({ data: submitableValues })
   }

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Status field name: ensure API expects jansStatus.

SDK models typically use jansStatus; setting only status may be ignored. Set both to be safe.

-    const submitableValues: CustomUser = {
+    const submitableValues: CustomUser = {
       userId: values.userId || '',
       mail: values.mail,
       displayName: values.displayName || '',
-      status: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
+      status: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
+      // Ensure SDK-compatible status field is also set
+      // @ts-expect-error: jansStatus may exist in SDK model
+      jansStatus: values.status as any,
       userPassword: values.userPassword as string | undefined,
       givenName: values.givenName || '',
       customAttributes: customAttributes,
     }

🏁 Script executed:

#!/bin/bash
# Confirm which field the SDK expects for user status
rg -nP 'interface\\s+CustomUser|type\\s+CustomUser' -g 'JansConfigApi*' -C3

Length of output: 148



🏁 Script executed:

#!/bin/bash
# Find where CustomUser is defined by checking imports in UserAddPage.tsx
echo "=== Step 1: Check imports in UserAddPage.tsx ==="
rg -n 'import.*CustomUser|from.*CustomUser' admin-ui/plugins/user-management/components/UserAddPage.tsx -A1

echo "=== Step 2: Search for CustomUser type/interface definition ==="
rg -nP 'type\s+CustomUser|interface\s+CustomUser' -C5

echo "=== Step 3: Check for jansStatus in SDK-related files ==="
rg -nP 'jansStatus|status.*field' --type=ts --type=tsx -C2 | head -50

Length of output: 1087



🏁 Script executed:

#!/bin/bash
# Get complete CustomUser interface from local types
echo "=== Full CustomUser interface from UserApiTypes.ts ==="
cat admin-ui/plugins/user-management/types/UserApiTypes.ts

echo ""
echo "=== Search for jansStatus anywhere in codebase ==="
rg -n 'jansStatus' --type ts

echo ""
echo "=== Check what fields are actually used in submitableValues (lines 102-112) ==="
sed -n '102,112p' admin-ui/plugins/user-management/components/UserAddPage.tsx

echo ""
echo "=== Search for status field references in user-related code ==="
rg -n 'status.*active.*inactive' admin-ui/plugins/user-management/ -C2

Length of output: 4458


Set both status and jansStatus fields when creating users.

The CustomUser interface defines both status and jansStatus fields, and UserForm.tsx prioritizes jansStatus when reading user data. The code at lines 102-112 only sets status, omitting jansStatus, which creates an asymmetry between write and read operations that may cause the API to mishandle the status field.

    const submitableValues: CustomUser = {
      userId: values.userId || '',
      mail: values.mail,
      displayName: values.displayName || '',
      status: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
+     jansStatus: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
      userPassword: values.userPassword as string | undefined,
      givenName: values.givenName || '',
      customAttributes: customAttributes,
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const submitableValues: CustomUser = {
      userId: values.userId || '',
      mail: values.mail,
      displayName: values.displayName || '',
      status: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
      jansStatus: values.status as 'active' | 'inactive' | 'expired' | 'register' | undefined,
      userPassword: values.userPassword as string | undefined,
      givenName: values.givenName || '',
      customAttributes: customAttributes,
    }
    createUserMutation.mutate({ data: submitableValues })
  }
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserAddPage.tsx around lines 102
to 112, the code only sets the status field when building submitableValues but
omits jansStatus; add a jansStatus property that mirrors the chosen status
(prefer values.jansStatus if present, otherwise fallback to values.status) and
cast it to the same union type ('active' | 'inactive' | 'expired' | 'register' |
undefined) so both fields are set consistently when calling
createUserMutation.mutate.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserDeviceDetailViewPage.tsx:

@@ -2,11 +2,11 @@ import React from 'react'
 import { Container, Row, Col } from 'Components'
 import GluuFormDetailRow from 'Routes/Apps/Gluu/GluuFormDetailRow'
 import customColors from '@/customColors'
-import { UserDeviceDetailViewPageProps, DeviceData } from '../../types/ComponentTypes'
+import { UserDeviceDetailViewPageProps } from '../types/ComponentTypes'
 
 const UserDeviceDetailViewPage = ({ row }: UserDeviceDetailViewPageProps) => {
   const { rowData } = row
-  const deviceData = rowData as DeviceData
+  const deviceData = rowData
   const DOC_SECTION = 'user'

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Property name drift vs. types/back-end shape (rpId, osName/osVersion)

UI now reads registrationData.rpId and deviceData.osName/osVersion, but types (DeviceData/FidoRegistrationEntry) still expose domain, os_name, os_version. This will break type-checking and risks undefined values at runtime.

Prefer backward-compatible reads and update types in a follow-up:

-  const deviceData = rowData
+  const deviceData = rowData
+  const domain = deviceData.registrationData?.rpId ?? deviceData.registrationData?.domain
+  const osName = deviceData.deviceData?.osName ?? deviceData.deviceData?.os_name
+  const osVersion = deviceData.deviceData?.osVersion ?? deviceData.deviceData?.os_version
@@
-            value={deviceData.registrationData?.rpId}
+            value={domain}
@@
-              value={deviceData.deviceData?.osName}
+              value={osName}
@@
-              value={deviceData.deviceData?.osVersion}
+              value={osVersion}

Then align interfaces in:

  • admin-ui/plugins/user-management/types/ComponentTypes.ts (DeviceData.deviceData fields)
  • admin-ui/plugins/user-management/types/UserApiTypes.ts (FidoRegistrationEntry) — see next comment.

Also applies to: 18-18, 63-63, 71-71


🏁 Script executed:

#!/bin/bash
# Find all usages to confirm both old and new keys exist
rg -nP --type=ts --type=tsx -C2 '\b(rpId|domain|osName|osVersion|os_name|os_version)\b' admin-ui/plugins/user-management

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search TypeScript/TSX files for both old and new property names
# Using glob pattern to match .tsx and .ts files
rg -nP -C2 '\b(rpId|domain|osName|osVersion|os_name|os_version)\b' --glob='**/*.{ts,tsx}' admin-ui/plugins/user-management

Length of output: 4097


Property name drift confirmed: types use snake_case, component reads camelCase

Verification confirms the issue. Types in ComponentTypes.ts (lines 124, 132-133) and UserApiTypes.ts (lines 38, 46-47, 51) define:

  • domain, os_name, os_version (snake_case)

But UserDeviceDetailViewPage.tsx reads:

  • Line 18: deviceData.registrationData?.rpId (missing from types entirely)
  • Line 63: deviceData.deviceData?.osName (type defines os_name)
  • Line 71: deviceData.deviceData?.osVersion (type defines os_version)

This will cause TypeScript errors and return undefined at runtime. The suggested backward-compatible fallback approach is sound; implement it and align type definitions in both files.

🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserDeviceDetailViewPage.tsx
around lines 5 to 10 (refs at lines 18, 63, 71), the component reads camelCase
properties (registrationData.rpId, deviceData.deviceData?.osName, osVersion)
while types define snake_case (os_name, os_version) and registrationData.rpId is
missing; update the component to use backward-compatible fallbacks (e.g.,
registrationData?.rpId ?? registrationData?.rp_id and deviceData?.osName ??
deviceData?.os_name, deviceData?.osVersion ?? deviceData?.os_version) so
runtime/TS errors are avoided, and then align the type definitions in
ComponentTypes.ts and UserApiTypes.ts to include the canonical camelCase names
(or both aliases) and add the missing rpId/rp_id field to the registrationData
type so TypeScript accurately reflects available payloads.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserEditPage.tsx:

@@ -1,141 +1,181 @@
-import React, { useEffect } from 'react'
-import { useNavigate } from 'react-router-dom'
+import React, { useEffect, useState } from 'react'
+import { useNavigate, useLocation } from 'react-router-dom'
 import { Container, CardBody, Card } from 'Components'
 import UserForm from './UserForm'
 import GluuAlert from 'Routes/Apps/Gluu/GluuAlert'
 import { useTranslation } from 'react-i18next'
-import { updateUser } from 'Plugins/user-management/redux/features/userSlice'
 import { useDispatch, useSelector } from 'react-redux'
 import { getAttributesRoot } from 'Redux/features/attributesSlice'
 import moment from 'moment'
 import GluuLoader from 'Routes/Apps/Gluu/GluuLoader'
 import { getPersistenceType } from 'Plugins/services/redux/features/persistenceTypeSlice'
-import {
-  UserEditPageState,
-  SubmitableUserValues,
-  UserEditFormValues,
-} from '../../types/ComponentTypes'
-import { PersonAttribute, GetUserOptions, CustomAttribute } from '../../types/UserApiTypes'
+import { UserEditPageState, UserEditFormValues } from '../types/ComponentTypes'
+import { PersonAttribute, CustomAttribute } from '../types/UserApiTypes'
+import { usePutUser, getGetUserQueryKey, GetAttributesParams } from 'JansConfigApi'
+import { useQueryClient } from '@tanstack/react-query'
+import { updateToast } from 'Redux/features/toastSlice'
+import { logUserUpdate, getErrorMessage } from '../helper/userAuditHelpers'
+import { triggerUserWebhook } from '../helper/userWebhookHelpers'
 
 function UserEditPage() {
   const dispatch = useDispatch()
   const navigate = useNavigate()
+  const location = useLocation()
+  const queryClient = useQueryClient()
   const { t } = useTranslation()
-  const userDetails = useSelector((state: UserEditPageState) => state.userReducer.selectedUserData)
+
+  const [userDetails] = useState(location.state?.selectedUser)
+  useEffect(() => {
+    if (!userDetails) {
+      navigate('/user/usersmanagement')
+    }
+  }, [userDetails, navigate])
+

🧹 Nitpick | 🔵 Trivial

Type userDetails for safety.

Give userDetails an explicit type to avoid accidental any and improve DX.

-  const [userDetails] = useState(location.state?.selectedUser)
+  const [userDetails] = useState<CustomUser | null>(location.state?.selectedUser ?? null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const [userDetails] = useState<CustomUser | null>(location.state?.selectedUser ?? null)
  useEffect(() => {
    if (!userDetails) {
      navigate('/user/usersmanagement')
    }
  }, [userDetails, navigate])
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserEditPage.tsx around lines 27
to 33, userDetails is untyped which can cause implicit any; give it an explicit
type (e.g. User | undefined) by either importing the existing User
interface/type from your types/models or declaring a local type that matches the
shape used, then change the useState call to useState<User |
undefined>(location.state?.selectedUser) (or cast location.state?.selectedUser
as User | undefined if necessary). Ensure the appropriate type import is added
at the top of the file so the navigate check continues to work with the typed
userDetails.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserEditPage.tsx:

@@ -1,141 +1,181 @@
-import React, { useEffect } from 'react'
-import { useNavigate } from 'react-router-dom'
+import React, { useEffect, useState } from 'react'
+import { useNavigate, useLocation } from 'react-router-dom'
 import { Container, CardBody, Card } from 'Components'
 import UserForm from './UserForm'
 import GluuAlert from 'Routes/Apps/Gluu/GluuAlert'
 import { useTranslation } from 'react-i18next'
-import { updateUser } from 'Plugins/user-management/redux/features/userSlice'
 import { useDispatch, useSelector } from 'react-redux'
 import { getAttributesRoot } from 'Redux/features/attributesSlice'
 import moment from 'moment'
 import GluuLoader from 'Routes/Apps/Gluu/GluuLoader'
 import { getPersistenceType } from 'Plugins/services/redux/features/persistenceTypeSlice'
-import {
-  UserEditPageState,
-  SubmitableUserValues,
-  UserEditFormValues,
-} from '../../types/ComponentTypes'
-import { PersonAttribute, GetUserOptions, CustomAttribute } from '../../types/UserApiTypes'
+import { UserEditPageState, UserEditFormValues } from '../types/ComponentTypes'
+import { PersonAttribute, CustomAttribute } from '../types/UserApiTypes'
+import { usePutUser, getGetUserQueryKey, GetAttributesParams } from 'JansConfigApi'
+import { useQueryClient } from '@tanstack/react-query'
+import { updateToast } from 'Redux/features/toastSlice'
+import { logUserUpdate, getErrorMessage } from '../helper/userAuditHelpers'
+import { triggerUserWebhook } from '../helper/userWebhookHelpers'
 
 function UserEditPage() {
   const dispatch = useDispatch()
   const navigate = useNavigate()
+  const location = useLocation()
+  const queryClient = useQueryClient()
   const { t } = useTranslation()
-  const userDetails = useSelector((state: UserEditPageState) => state.userReducer.selectedUserData)
+
+  const [userDetails] = useState(location.state?.selectedUser)
+  useEffect(() => {
+    if (!userDetails) {
+      navigate('/user/usersmanagement')
+    }
+  }, [userDetails, navigate])
+
   const personAttributes = useSelector(
     (state: UserEditPageState) => state.attributesReducerRoot.items,
   )
-  const redirectToUserListPage = useSelector(
-    (state: UserEditPageState) => state.userReducer.redirectToUserListPage,
-  )
   const loadingAttributes = useSelector(
     (state: UserEditPageState) => state.attributesReducerRoot.initLoading,
   )
 
-  const options: GetUserOptions = {}
+  const options: Partial<GetAttributesParams> = {}
   useEffect(() => {
     dispatch(getPersistenceType())
-  }, [])
+  }, [dispatch])
 
   const persistenceType = useSelector(
     (state: UserEditPageState) => state.persistenceTypeReducer.type,
   )
 
-  useEffect(() => {
-    if (redirectToUserListPage) {
-      navigate('/user/usersmanagement')
+  const updateUserMutation = usePutUser({
+    mutation: {
+      onSuccess: async (data, variables) => {
+        dispatch(updateToast(true, 'success', t('messages.user_updated_successfully')))
+        await logUserUpdate(data, variables.data)
+        await triggerUserWebhook(data)
+        queryClient.invalidateQueries({ queryKey: getGetUserQueryKey() })
+        navigate('/user/usersmanagement')
+      },
+      onError: (error: unknown) => {
+        const errMsg = getErrorMessage(error)
+        dispatch(updateToast(true, 'error', errMsg))
+      },
+    },
+  })
+
+  const createCustomAttributes = (values: UserEditFormValues): CustomAttribute[] => {
+    if (!values) {
+      return []
     }
-  }, [redirectToUserListPage])
-
-  const createCustomAttributes = (values: UserEditFormValues) => {
-    const customAttributes = []
-    if (values) {
-      for (const key in values) {
-        const customAttribute = personAttributes.filter((e: PersonAttribute) => e.name == key)
-        if (personAttributes.some((e: PersonAttribute) => e.name == key)) {
-          let obj = {}
-          if (!customAttribute[0]?.oxMultiValuedAttribute) {
-            const val = []
-            if (key != 'birthdate') {
-              val.push(values[key])
-            } else {
-              values[key] ? val.push(moment(values[key], 'YYYY-MM-DD').format('YYYY-MM-DD')) : null
-            }
-            obj = {
-              name: key,
-              multiValued: false,
-              values: val,
-            }
-          } else {
-            const valE = []
-            if (values[key] && Array.isArray(values[key])) {
-              for (const i in values[key] as string[]) {
-                const currentValue = (values[key] as string[])[i]
-                if (typeof currentValue == 'object') {
-                  valE.push(currentValue[key])
-                } else {
-                  valE.push(currentValue)
-                }
+
+    const result: CustomAttribute[] = []
+
+    Object.keys(values).forEach((attributeName) => {
+      const attributeDefinition = personAttributes.find(
+        (attribute: PersonAttribute) => attribute.name === attributeName,
+      )
+
+      if (!attributeDefinition) {
+        return
+      }
+
+      const isMultiValued = Boolean(attributeDefinition.oxMultiValuedAttribute)
+      const rawValue = values[attributeName]
+
+      if (!isMultiValued) {
+        let normalized: string | null = null
+        if (typeof rawValue === 'string') {
+          normalized = rawValue
+        } else if (Array.isArray(rawValue)) {
+          normalized = (rawValue[0] as string | undefined) ?? null
+        }
+
+        const singleValue =
+          attributeName === 'birthdate' && normalized
+            ? moment(normalized, 'YYYY-MM-DD').format('YYYY-MM-DD')
+            : (normalized ?? '')
+
+        const customAttribute: CustomAttribute = {
+          name: attributeName,
+          multiValued: false,
+          values: singleValue ? [singleValue] : [],
+        }
+        result.push(customAttribute)
+      } else {
+        let multiValues: string[] = []
+        if (Array.isArray(rawValue)) {
+          multiValues = (rawValue as unknown[])
+            .map((entry) => {
+              if (typeof entry === 'string') {
+                return entry
               }
-            }
-            obj = {
-              name: key,
-              multiValued: true,
-              values: valE,
-            }
-          }
-          customAttributes.push(obj)
+              if (entry && typeof entry === 'object') {
+                const record = entry as Record<string, unknown>
+                const maybe = record.value ?? record[attributeName]
+                return typeof maybe === 'string' ? maybe : ''
+              }
+              return ''
+            })
+            .filter((v): v is string => Boolean(v))
+        }
+
+        const customAttribute: CustomAttribute = {
+          name: attributeName,
+          multiValued: true,
+          values: multiValues,
         }
+        result.push(customAttribute)
       }
-      return customAttributes
-    }
+    })
+
+    return result
   }

⚠️ Potential issue | 🟡 Minor

Attribute normalization looks solid; minor nit.

Consider trimming string values to avoid trailing-space diffs and sending empty arrays only when values are truly missing.

-          normalized = rawValue
+          normalized = rawValue.trim()
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserEditPage.tsx around lines 66
to 132, trim string inputs and drop blank-only entries so we don't send values
containing only whitespace or unnecessary empty arrays: when handling
non-multiValued attributes, trim the normalized string and treat it as missing
if empty (so set values to [] only when truly empty); when handling multiValued
attributes, trim each string entry, filter out empty strings after trimming, and
only include the resulting values array (possibly empty) if there are real
values; apply the same trimming when extracting values from objects
(record.value or record[attributeName]).

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserForm.tsx:

@@ -23,28 +27,43 @@ import {
   UserEditFormValues,
 } from '../types/ComponentTypes'
 import { ThemeContext as ThemeContextType } from '../types/CommonTypes'
-import { PersonAttribute, ChangeUserPasswordPayload, GetUserOptions } from '../types/UserApiTypes'
+import { PersonAttribute } from '../types/UserApiTypes'
 
-function UserForm({ onSubmitData }: UserFormProps) {
+// eslint-disable-next-line sonarjs/cognitive-complexity

🛠️ Refactor suggestion | 🟠 Major

Address the cognitive complexity.

The sonarjs/cognitive-complexity disable indicates this function is complex. Consider breaking it into smaller, focused functions (e.g., extract password change logic, custom attributes initialization, form submission handling) to improve maintainability.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserForm.tsx:

@@ -102,35 +128,25 @@ function UserForm({ onSubmitData }: UserFormProps) {
 
   const submitChangePassword = (usermessage: string) => {
     if (!userDetails?.inum || !formik.values.userPassword) return
-
-    const submitableValue: ChangeUserPasswordPayload = {
+    const patchOperations = [
+      {
+        op: 'replace',
+        path: '/userPassword',
+        value: formik.values.userPassword,
+      },
+    ]
+    changePasswordMutation.mutate({
       inum: userDetails.inum,
-      jsonPatchString: '[]',
-      customAttributes: [
-        {
-          name: 'userPassword',
-          multiValued: false,
-          values: [formik.values.userPassword as string],
-        },
-      ],
-    }
-    submitableValue.performedOn = {
-      user_inum: userDetails.inum,
-      userId: userDetails.displayName as string,
-    }
-    // Set action_message for audit logging
-    if (usermessage) {
-      submitableValue.action_message = usermessage
-    }
-    dispatch(changeUserPassword(submitableValue))
-    setPasswordModal(!passwordmodal)
+      data: patchOperations,
+    })
+    setPasswordModal(!passwordModal)
     toggleChangePasswordModal()
   }
   const submitForm = (usermessage: string) => {
     toggle()
     onSubmitData(formik.values, modifiedFields, usermessage)
   }
-  const loading = useSelector((state: UserFormState) => state.userReducer.loading)
+  const loading = false

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify hardcoded loading state is intentional.

The loading constant is hardcoded to false, which will prevent the loading spinner from displaying during operations. If the password change mutation or other async operations should show loading state, consider using changePasswordMutation.isPending or equivalent React Query loading states.


🏁 Script executed:

#!/bin/bash
# Check if loading state was previously derived from Redux or other sources
rg -n -B3 -A3 'loading.*=.*false' --type=tsx --type=ts

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find the UserForm.tsx file and view it
fd -e tsx -e ts "UserForm" --exec cat {}

Length of output: 19797


🏁 Script executed:

#!/bin/bash
# Search for mutation-related patterns (React Query, Redux, etc.)
rg -n "isPending|isLoading|mutation" admin-ui/plugins/user-management/components/ -A 2 -B 2

Length of output: 4379


🏁 Script executed:

#!/bin/bash
# Check for Redux connect or useSelector patterns
rg -n "useSelector|connect|mapStateToProps" admin-ui/plugins/user-management/components/UserForm.tsx -A 2 -B 2

Length of output: 524


Replace hardcoded loading state with mutation isPending property.

The loading variable at line 149 is hardcoded to false, which prevents the loader from displaying during password changes. Since changePasswordMutation is defined using React Query's usePatchUserByInum hook, replace the hardcoded value with changePasswordMutation.isPending to properly track async operations:

const loading = changePasswordMutation.isPending

This ensures the GluuLoader component at line 244 displays blocking feedback while the password mutation is in progress.

🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserForm.tsx around line 149, the
loading variable is hardcoded to false which prevents the GluuLoader from
showing during async password changes; replace the hardcoded assignment with the
mutation's pending state by setting loading to changePasswordMutation.isPending
so the loader at line 244 correctly reflects the in-progress state of
changePasswordMutation.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserForm.tsx:

@@ -162,21 +178,15 @@ function UserForm({ onSubmitData }: UserFormProps) {
     'sn',
   ]
   const getCustomAttributeById = (id: string) => {
-    let claimData = null
-    for (const i in personAttributes) {
-      if (personAttributes[i].name == id) {
-        claimData = personAttributes[i]
-      }
-    }
-    return claimData
+    const match = personAttributes.find((attr) => attr.name == id)
+    return match || null
   }

⚠️ Potential issue | 🟡 Minor

Use strict equality.

Line 181 uses loose equality (==) which can lead to unexpected type coercion. Use strict equality (===) instead.

Apply this diff:

 const getCustomAttributeById = (id: string) => {
-  const match = personAttributes.find((attr) => attr.name == id)
+  const match = personAttributes.find((attr) => attr.name === id)
   return match || null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const getCustomAttributeById = (id: string) => {
    const match = personAttributes.find((attr) => attr.name === id)
    return match || null
  }
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/components/UserForm.tsx around lines 180 to
183, the comparison uses loose equality (==) which allows type coercion; change
the comparison to strict equality (===) so the find callback uses
personAttributes.find((attr) => attr.name === id) and return the matched
attribute or null as before.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/components/UserList.tsx:

@@ -137,21 +210,21 @@ function UserList(): JSX.Element {
         }
       }) || []
     setOTPDevicesList(otpDevices)
-    const payload: User2FAPayload = {
-      username: (row.givenName || '').toLowerCase(),
-      token: token,
+
+    // Fetch FIDO2 details
+    if (row.userId) {
+      await refetchFido2Details()
     }
-    dispatch(getUser2FADetails(payload))
-    setIsDetailModalOpen(!isViewDetailModalOpen)
+    setIsViewDetailModalOpen(!isViewDetailModalOpen)
   }
 
   function handleGoToUserEditPage(row: UserTableRowData): void {
-    dispatch(setSelectedUserData(row as unknown as CustomUser))
-    navigate(`/user/usermanagement/edit/:${row.tableData?.uuid || ''}`)
+    const userData = row as unknown as CustomUser
+    navigate(`/user/usermanagement/edit/:${row.tableData?.uuid || ''}`, {
+      state: { selectedUser: userData },
+    })
   }

⚠️ Potential issue | 🔴 Critical

Edit navigation path includes a literal colon; likely breaks routing.

Colon is for route definitions, not actual URLs. Either drop it or pass only state.

-    navigate(`/user/usermanagement/edit/:${row.tableData?.uuid || ''}`, {
-      state: { selectedUser: userData },
-    })
+    navigate('/user/usermanagement/edit', {
+      state: { selectedUser: userData },
+    })

If a param is required, use a concrete value: /user/usermanagement/edit/${encodeURIComponent(row.inum ?? row.userId ?? '')} (no colon).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    navigate('/user/usermanagement/edit', {
      state: { selectedUser: userData },
    })
  }

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/helper/userAuditHelpers.ts:

@@ -0,0 +1,230 @@
+import store from 'Redux/store'
+import { logAuditUserAction } from 'Utils/AuditLogger'
+import { FETCH, DELETION, UPDATE, CREATE } from '../../../app/audit/UserActionType'
+import { API_USERS } from '../../../app/audit/Resources'
+
+export interface AuditLog {
+  headers: {
+    Authorization?: string
+  }
+  client_id?: string
+  ip_address?: string
+  status?: string
+  performedBy?: {
+    user_inum: string
+    userId: string
+  }
+  message?: string
+}
+
+export interface AuthState {
+  token: {
+    access_token: string
+  }
+  issuer: string
+  userinfo_jwt: string
+  config: {
+    clientId: string
+  }
+  location: {
+    IPv4: string
+  }
+  userinfo: {
+    name: string
+    inum: string
+  }
+}
+
+/**
+ * Initialize audit log with user information from Redux store
+ */
+export function initAudit(): AuditLog {
+  const state = store.getState() as any
+  const authReducer: AuthState = state.authReducer
+  const auditlog: AuditLog = {
+    headers: {},
+  }
+  const client_id = authReducer.config?.clientId || ''
+  const ip_address = authReducer.location?.IPv4 || ''
+  const userinfo = authReducer.userinfo
+  const author = userinfo ? userinfo.name : '-'
+  const inum = userinfo ? userinfo.inum : '-'
+  const token = authReducer.token?.access_token || ''
+  auditlog.client_id = client_id
+  auditlog.ip_address = ip_address
+  auditlog.status = 'success'
+  auditlog.performedBy = { user_inum: inum, userId: author }
+  auditlog.headers.Authorization = `Bearer ${token}`
+
+  return auditlog
+}
+
+export async function logUserCreation(data: any, payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+
+    const auditPayload = { ...payload }
+    delete auditPayload.userPassword // Don't log passwords
+
+    // Ensure modifiedFields/performedOn are inside payload so they are hoisted
+    if (payload?.modifiedFields && !auditPayload.modifiedFields) {
+      auditPayload.modifiedFields = payload.modifiedFields
+    }
+    if (payload?.performedOn && !auditPayload.performedOn) {
+      auditPayload.performedOn = payload.performedOn
+    }
+
+    const message =
+      payload?.action?.action_message ||
+      payload?.action_message ||
+      payload?.message ||
+      'Created user'
+
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: CREATE,
+      resource: API_USERS,
+      message,
+      client_id,
+      payload: auditPayload,
+    })
+  } catch (error) {
+    console.error('Failed to log user creation:', error)
+  }
+}
+
+export async function logUserUpdate(data: any, payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+    const auditPayload = { ...payload }
+    if (auditPayload.customAttributes && auditPayload.customAttributes[0]) {
+      delete auditPayload.customAttributes[0].values
+    }
+    if (payload?.modifiedFields && !auditPayload.modifiedFields) {
+      auditPayload.modifiedFields = payload.modifiedFields
+    }
+    if (payload?.performedOn && !auditPayload.performedOn) {
+      auditPayload.performedOn = payload.performedOn
+    }
+    const message =
+      payload?.action?.action_message ||
+      payload?.action_message ||
+      payload?.message ||
+      'Updated user'
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: UPDATE,
+      resource: API_USERS,
+      message,
+      client_id,
+      payload: auditPayload,
+    })
+  } catch (error) {
+    console.error('Failed to log user update:', error)
+  }
+}

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify payload sanitization is consistent.

logUserUpdate doesn't sanitize userPassword from the payload (unlike logUserCreation at line 71). If password updates go through a different flow (like logPasswordChange), this is correct. Otherwise, ensure sensitive data isn't logged.


🏁 Script executed:

#!/bin/bash
# Search for update user mutations/calls to verify password handling
rg -nP -C3 'logUserUpdate|patchUser|updateUser' --type=ts --type=tsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Corrected search - remove tsx type restriction and search the helpers file
cat admin-ui/plugins/user-management/helper/userAuditHelpers.ts

Length of output: 6538


🏁 Script executed:

#!/bin/bash
# Search for calls to logUserUpdate and password-related patterns
rg -nP -C5 'logUserUpdate' --type=ts

Length of output: 5730


🏁 Script executed:

#!/bin/bash
# Search for password sanitization patterns in the codebase
rg -nP -C3 'delete.*userPassword|sanitize|password' --type=ts

Length of output: 21918


🏁 Script executed:

#!/bin/bash
# Check how update payloads are constructed in UserForm.tsx
rg -nP -C10 'patchOperations|variables\.data' admin-ui/plugins/user-management/components/UserForm.tsx | head -80

Length of output: 1748


🏁 Script executed:

#!/bin/bash
# Check if userPassword is ever added to update operations
rg -nP -C5 'userPassword' admin-ui/plugins/user-management/components/UserForm.tsx | grep -A5 -B5 'patchOperations\|submitForm'

Length of output: 534


🏁 Script executed:

#!/bin/bash
# Look at UserList.tsx to see what data is sent in the update mutation
rg -nP -C10 'usePutUser\|updateUserMutation' admin-ui/plugins/user-management/components/UserList.tsx | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get UserList.tsx file to see updateUserMutation context
sed -n '130,150p' admin-ui/plugins/user-management/components/UserList.tsx

Length of output: 676


🏁 Script executed:

#!/bin/bash
# Search for onSubmitData in UserForm to see what data structure is built for PUT
rg -nP -C15 'onSubmitData' admin-ui/plugins/user-management/components/UserForm.tsx | head -100

Length of output: 2545


🏁 Script executed:

#!/bin/bash
# Check UserEditPage.tsx updateUserMutation to understand PUT data flow
rg -nP -C10 'updateUserMutation\|usePutUser' admin-ui/plugins/user-management/components/UserEditPage.tsx

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where UserForm is used and onSubmitData is implemented
rg -nP 'onSubmitData|<UserForm' admin-ui/plugins/user-management/components/UserList.tsx admin-ui/plugins/user-management/components/UserEditPage.tsx

Length of output: 143


🏁 Script executed:

#!/bin/bash
# Search for updateUserMutation.mutate call to see what payload is sent
rg -nP -C10 'updateUserMutation\.mutate|mutate\s*\(\s*\{' admin-ui/plugins/user-management/components/UserList.tsx admin-ui/plugins/user-management/components/UserEditPage.tsx

Length of output: 6327


🏁 Script executed:

#!/bin/bash
# Look at entire UserList.tsx file sections handling user updates
sed -n '80,200p' admin-ui/plugins/user-management/components/UserList.tsx

Length of output: 3975


Consistency in password sanitization: logUserCreation sanitizes but logUserUpdate doesn't.

While current code flows keep passwords separate (password changes use logPasswordChange), the inconsistency in defensive measures should be addressed. logUserCreation sanitizes userPassword at line 71, but logUserUpdate lacks equivalent sanitization. For consistency and to guard against future payload changes, apply the same sanitization.

Add to logUserUpdate (lines 101–135) after line 109:

delete auditPayload.userPassword
delete auditPayload.userConfirmPassword
🤖 Prompt for AI Agents
In admin-ui/plugins/user-management/helper/userAuditHelpers.ts around lines 101
to 135, add the same defensive password sanitization used in logUserCreation:
after line 109 remove any sensitive password fields from auditPayload by
deleting auditPayload.userPassword and auditPayload.userConfirmPassword so they
are not included in the audit payload before calling logAuditUserAction.

@mo-auto
Copy link
Member

mo-auto commented Oct 20, 2025

On admin-ui/plugins/user-management/helper/userAuditHelpers.ts:

@@ -0,0 +1,230 @@
+import store from 'Redux/store'
+import { logAuditUserAction } from 'Utils/AuditLogger'
+import { FETCH, DELETION, UPDATE, CREATE } from '../../../app/audit/UserActionType'
+import { API_USERS } from '../../../app/audit/Resources'
+
+export interface AuditLog {
+  headers: {
+    Authorization?: string
+  }
+  client_id?: string
+  ip_address?: string
+  status?: string
+  performedBy?: {
+    user_inum: string
+    userId: string
+  }
+  message?: string
+}
+
+export interface AuthState {
+  token: {
+    access_token: string
+  }
+  issuer: string
+  userinfo_jwt: string
+  config: {
+    clientId: string
+  }
+  location: {
+    IPv4: string
+  }
+  userinfo: {
+    name: string
+    inum: string
+  }
+}
+
+/**
+ * Initialize audit log with user information from Redux store
+ */
+export function initAudit(): AuditLog {
+  const state = store.getState() as any
+  const authReducer: AuthState = state.authReducer
+  const auditlog: AuditLog = {
+    headers: {},
+  }
+  const client_id = authReducer.config?.clientId || ''
+  const ip_address = authReducer.location?.IPv4 || ''
+  const userinfo = authReducer.userinfo
+  const author = userinfo ? userinfo.name : '-'
+  const inum = userinfo ? userinfo.inum : '-'
+  const token = authReducer.token?.access_token || ''
+  auditlog.client_id = client_id
+  auditlog.ip_address = ip_address
+  auditlog.status = 'success'
+  auditlog.performedBy = { user_inum: inum, userId: author }
+  auditlog.headers.Authorization = `Bearer ${token}`
+
+  return auditlog
+}
+
+export async function logUserCreation(data: any, payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+
+    const auditPayload = { ...payload }
+    delete auditPayload.userPassword // Don't log passwords
+
+    // Ensure modifiedFields/performedOn are inside payload so they are hoisted
+    if (payload?.modifiedFields && !auditPayload.modifiedFields) {
+      auditPayload.modifiedFields = payload.modifiedFields
+    }
+    if (payload?.performedOn && !auditPayload.performedOn) {
+      auditPayload.performedOn = payload.performedOn
+    }
+
+    const message =
+      payload?.action?.action_message ||
+      payload?.action_message ||
+      payload?.message ||
+      'Created user'
+
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: CREATE,
+      resource: API_USERS,
+      message,
+      client_id,
+      payload: auditPayload,
+    })
+  } catch (error) {
+    console.error('Failed to log user creation:', error)
+  }
+}
+
+export async function logUserUpdate(data: any, payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+    const auditPayload = { ...payload }
+    if (auditPayload.customAttributes && auditPayload.customAttributes[0]) {
+      delete auditPayload.customAttributes[0].values
+    }
+    if (payload?.modifiedFields && !auditPayload.modifiedFields) {
+      auditPayload.modifiedFields = payload.modifiedFields
+    }
+    if (payload?.performedOn && !auditPayload.performedOn) {
+      auditPayload.performedOn = payload.performedOn
+    }
+    const message =
+      payload?.action?.action_message ||
+      payload?.action_message ||
+      payload?.message ||
+      'Updated user'
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: UPDATE,
+      resource: API_USERS,
+      message,
+      client_id,
+      payload: auditPayload,
+    })
+  } catch (error) {
+    console.error('Failed to log user update:', error)
+  }
+}
+
+export async function logUserDeletion(inum: string, userData?: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+
+    const payload = userData || { inum }
+
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: DELETION,
+      resource: API_USERS,
+      message: 'Deleted user',
+      client_id,
+      payload,
+    })
+  } catch (error) {
+    console.error('Failed to log user deletion:', error)
+  }
+}
+
+export async function logUserFetch(payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: FETCH,
+      resource: API_USERS,
+      message: 'Fetched users',
+      client_id,
+      payload,
+    })
+  } catch (error) {
+    console.error('Failed to log user fetch:', error)
+  }
+}
+
+export async function logPasswordChange(inum: string, payload: any): Promise<void> {
+  try {
+    const state = store.getState() as any
+    const authReducer: AuthState = state.authReducer
+    const token = authReducer.token?.access_token || ''
+    const client_id = authReducer.config?.clientId || ''
+    const userinfo = authReducer.userinfo
+
+    const auditPayload = { ...payload }
+    if (auditPayload.customAttributes && auditPayload.customAttributes[0]) {
+      delete auditPayload.customAttributes[0].values
+    }
+    if (payload?.modifiedFields && !auditPayload.modifiedFields) {
+      auditPayload.modifiedFields = payload.modifiedFields
+    }
+    if (payload?.performedOn && !auditPayload.performedOn) {
+      auditPayload.performedOn = payload.performedOn
+    }
+
+    await logAuditUserAction({
+      token,
+      userinfo,
+      action: UPDATE,
+      resource: API_USERS,
+      message: 'Password changed',
+      client_id,
+      payload: auditPayload,
+    })
+  } catch (error) {
+    console.error('Failed to log password change:', error)
+  }
+}

⚠️ Potential issue | 🔴 Critical

Critical: Password may be logged in audit trail.

The logPasswordChange function receives the payload parameter which, based on usage in UserForm.tsx line 57, contains the patch operations including the plaintext password value. This sensitive data should be sanitized before logging.

Apply this diff to sanitize password from audit payload:

 export async function logPasswordChange(inum: string, payload: any): Promise<void> {
   try {
     const state = store.getState() as any
     const authReducer: AuthState = state.authReducer
     const token = authReducer.token?.access_token || ''
     const client_id = authReducer.config?.clientId || ''
     const userinfo = authReducer.userinfo
 
     const auditPayload = { ...payload }
+    // Sanitize password from patch operations
+    if (Array.isArray(auditPayload)) {
+      auditPayload.forEach((op: any) => {
+        if (op.path === '/userPassword') {
+          op.value = '[REDACTED]'
+        }
+      })
+    }
     if (auditPayload.customAttributes && auditPayload.customAttributes[0]) {
       delete auditPayload.customAttributes[0].values
     }

Committable suggestion skipped: line range outside the PR's diff.

@GluuFederation GluuFederation deleted a comment from sonarqubecloud bot Oct 20, 2025
@GluuFederation GluuFederation deleted a comment from mo-auto Oct 20, 2025
moabu
moabu previously approved these changes Oct 21, 2025
Copy link
Contributor

@duttarnab duttarnab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@syntrydy syntrydy requested a review from duttarnab October 25, 2025 11:44
@sonarqubecloud
Copy link

@duttarnab duttarnab merged commit d0c8a7a into main Oct 27, 2025
7 checks passed
@duttarnab duttarnab deleted the fix-adminui-issues-2337 branch October 27, 2025 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp-admin-ui Component affected by issue or PR kind-bug Issue or PR is a bug in existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(admin-ui): Use typescript generated client for USERS plugin

4 participants