fix(admin-ui): Use typescript generated client for USERS plugin#2338
fix(admin-ui): Use typescript generated client for USERS plugin#2338
Conversation
|
|
On @@ -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 |
|
On @@ -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
},
}
|
|
On @@ -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
+ }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
}
🤖 Prompt for AI Agents |
|
On @@ -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
🤖 Prompt for AI Agents |
|
On @@ -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)])
+}
🤖 Prompt for AI Agents |
|
On @@ -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])🧩 Analysis chainBug: 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 🏁 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-managementLength 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 -20Length 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 -10Length 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 This same pattern appears in two places:
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 ( 📝 Committable suggestion
🤖 Prompt for AI Agents |
|
On @@ -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)
})
})🧩 Analysis chainVerify 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:
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/' -C2Length 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 -20Length 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 -10Length 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:
|
|
On @@ -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
+ }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 |
|
On @@ -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
🤖 Prompt for AI Agents |
|
On @@ -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 })
}🧩 Analysis chainStatus 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*' -C3Length 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 -50Length 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/ -C2Length of output: 4458 Set both The 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
🤖 Prompt for AI Agents |
|
On @@ -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'🧩 Analysis chainProperty name drift vs. types/back-end shape (rpId, osName/osVersion) UI now reads 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:
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-managementLength 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-managementLength of output: 4097 Property name drift confirmed: types use snake_case, component reads camelCase Verification confirms the issue. Types in
But
This will cause TypeScript errors and return 🤖 Prompt for AI Agents |
|
On @@ -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
🤖 Prompt for AI Agents |
|
On @@ -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
}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 |
|
On @@ -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 |
|
On @@ -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🧩 Analysis chainVerify hardcoded loading state is intentional. The 🏁 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=tsLength 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 2Length 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 2Length of output: 524 Replace hardcoded loading state with mutation isPending property. The const loading = changePasswordMutation.isPendingThis ensures the 🤖 Prompt for AI Agents |
|
On @@ -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
}Use strict equality. Line 181 uses loose equality ( 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
🤖 Prompt for AI Agents |
|
On @@ -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 },
+ })
}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: 📝 Committable suggestion
|
|
On @@ -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)
+ }
+}🧩 Analysis chainVerify payload sanitization is consistent.
🏁 Script executed: #!/bin/bash
# Search for update user mutations/calls to verify password handling
rg -nP -C3 'logUserUpdate|patchUser|updateUser' --type=ts --type=tsxLength 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.tsLength of output: 6538 🏁 Script executed: #!/bin/bash
# Search for calls to logUserUpdate and password-related patterns
rg -nP -C5 'logUserUpdate' --type=tsLength of output: 5730 🏁 Script executed: #!/bin/bash
# Search for password sanitization patterns in the codebase
rg -nP -C3 'delete.*userPassword|sanitize|password' --type=tsLength 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 -80Length 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 -60Length 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.tsxLength 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 -100Length 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.tsxLength 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.tsxLength 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.tsxLength 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.tsxLength of output: 3975 Consistency in password sanitization: While current code flows keep passwords separate (password changes use Add to 🤖 Prompt for AI Agents |
|
On @@ -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)
+ }
+}Critical: Password may be logged in audit trail. The 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
}
|
|





#2337
closes #2337