diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.tsx b/admin-ui/app/routes/Dashboards/DashboardPage.tsx index f9d4f6df2e..80676ea4bb 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.tsx +++ b/admin-ui/app/routes/Dashboards/DashboardPage.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo, useCallback, useContext } from 're import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useMediaQuery } from 'react-responsive' -import dayjs from 'dayjs' import type { Dayjs } from 'dayjs' import Grid from '@mui/material/Grid' import Paper from '@mui/material/Paper' @@ -33,6 +32,14 @@ import { useDashboardLicense, useDashboardClients, useDashboardLockStats } from import styles from './styles' import { StatusIndicator, SummaryCard, UserInfoItem } from './components' import GluuText from 'Routes/Apps/Gluu/GluuText' +import { + isAfterDate, + isBeforeDate, + createDate, + subtractDate, + formatDate as formatDayjsDate, + DATE_FORMATS, +} from '@/utils/dayjsUtils' interface RootState { authReducer: AuthState @@ -80,8 +87,8 @@ const DashboardPage = () => { isDark, }) - const [startDate, setStartDate] = useState(dayjs().subtract(3, 'months')) - const [endDate, setEndDate] = useState(dayjs()) + const [startDate, setStartDate] = useState(() => subtractDate(createDate(), 3, 'months')) + const [endDate, setEndDate] = useState(() => createDate()) const debouncedStartDate = useDebounce(startDate, 400) const debouncedEndDate = useDebounce(endDate, 400) @@ -310,12 +317,12 @@ const DashboardPage = () => { if (type === 'start') { setStartDate(date) - if (date.isAfter(endDate)) { + if (isAfterDate(date, endDate)) { setEndDate(date) } } else { setEndDate(date) - if (date.isBefore(startDate)) { + if (isBeforeDate(date, startDate)) { setStartDate(date) } } @@ -335,8 +342,8 @@ const DashboardPage = () => { const dateMonths = useMemo( () => ({ - start: debouncedStartDate.format('YYYYMM'), - end: debouncedEndDate.format('YYYYMM'), + start: formatDayjsDate(debouncedStartDate, DATE_FORMATS.MONTH_KEY), + end: formatDayjsDate(debouncedEndDate, DATE_FORMATS.MONTH_KEY), }), [debouncedStartDate, debouncedEndDate], ) diff --git a/admin-ui/app/routes/Dashboards/DateRange/index.tsx b/admin-ui/app/routes/Dashboards/DateRange/index.tsx index fc002973ca..6322a8de17 100644 --- a/admin-ui/app/routes/Dashboards/DateRange/index.tsx +++ b/admin-ui/app/routes/Dashboards/DateRange/index.tsx @@ -1,6 +1,7 @@ import React, { memo, useMemo, useContext } from 'react' import { useTranslation } from 'react-i18next' import type { Dayjs } from 'dayjs' +import { isSameDate } from '@/utils/dayjsUtils' import Grid from '@mui/material/Grid' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' @@ -265,12 +266,12 @@ const DateRange = memo( (prevProps, nextProps) => { const startDateSame = prevProps.startDate && nextProps.startDate - ? prevProps.startDate.isSame(nextProps.startDate) + ? isSameDate(prevProps.startDate, nextProps.startDate) : prevProps.startDate === nextProps.startDate const endDateSame = prevProps.endDate && nextProps.endDate - ? prevProps.endDate.isSame(nextProps.endDate) + ? isSameDate(prevProps.endDate, nextProps.endDate) : prevProps.endDate === nextProps.endDate return ( diff --git a/admin-ui/app/utilities.tsx b/admin-ui/app/utilities.tsx index 76e500d5fa..03b830c2c8 100755 --- a/admin-ui/app/utilities.tsx +++ b/admin-ui/app/utilities.tsx @@ -5,7 +5,7 @@ declare const require: { useSubdirectories: boolean, regExp: RegExp, ) => { - keys: () => string[]; + keys: () => string[] (id: string): any } } diff --git a/admin-ui/app/utils/dayjsUtils.ts b/admin-ui/app/utils/dayjsUtils.ts index 99a95961ab..286506b87f 100644 --- a/admin-ui/app/utils/dayjsUtils.ts +++ b/admin-ui/app/utils/dayjsUtils.ts @@ -3,20 +3,28 @@ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' import customParseFormat from 'dayjs/plugin/customParseFormat' import type { Dayjs, OpUnitType, ManipulateType } from 'dayjs' -// Extend dayjs with plugins dayjs.extend(isSameOrBefore) dayjs.extend(customParseFormat) -// Re-export types for convenience export type { Dayjs } from 'dayjs' -/** - * Check if a date is the same as or before another date - * @param date - The date to check - * @param compareDate - The date to compare against - * @param unit - Optional unit of comparison (e.g., 'month', 'day', 'year') - * @returns true if date is same or before compareDate - */ +export const DATE_FORMATS = { + DATE_ONLY: 'YYYY-MM-DD', + DATETIME_SECONDS: 'YYYY-MM-DD HH:mm:ss', + DATETIME_AMPM: 'YYYY-MM-DD h:mm:ss A', + DATETIME_LONG: 'ddd, MMM DD, YYYY h:mm:ss A', + MONTH_KEY: 'YYYYMM', + TOKEN_DATETIME: 'YYYY/DD/MM HH:mm:ss', +} as const + +export const diffDate = ( + dateA: string | number | Date | Dayjs, + dateB: string | number | Date | Dayjs, + unit?: OpUnitType, +): number => { + return dayjs(dateA).diff(dateB, unit) +} + export const isSameOrBeforeDate = ( date: string | number | Date | Dayjs, compareDate: string | number | Date | Dayjs, @@ -25,13 +33,6 @@ export const isSameOrBeforeDate = ( return dayjs(date).isSameOrBefore(compareDate, unit) } -/** - * Check if a date is after another date - * @param date - The date to check - * @param compareDate - The date to compare against - * @param unit - Optional unit of comparison (e.g., 'month', 'day', 'year') - * @returns true if date is after compareDate - */ export const isAfterDate = ( date: string | number | Date | Dayjs, compareDate: string | number | Date | Dayjs, @@ -40,13 +41,6 @@ export const isAfterDate = ( return dayjs(date).isAfter(compareDate, unit) } -/** - * Check if a date is before another date - * @param date - The date to check - * @param compareDate - The date to compare against - * @param unit - Optional unit of comparison (e.g., 'month', 'day', 'year') - * @returns true if date is before compareDate - */ export const isBeforeDate = ( date: string | number | Date | Dayjs, compareDate: string | number | Date | Dayjs, @@ -55,13 +49,6 @@ export const isBeforeDate = ( return dayjs(date).isBefore(compareDate, unit) } -/** - * Check if a date is the same as another date - * @param date - The date to check - * @param compareDate - The date to compare against - * @param unit - Optional unit of comparison (e.g., 'month', 'day', 'year') - * @returns true if dates are the same - */ export const isSameDate = ( date: string | number | Date | Dayjs, compareDate: string | number | Date | Dayjs, @@ -70,30 +57,11 @@ export const isSameDate = ( return dayjs(date).isSame(compareDate, unit) } -/** - * Check if a date is valid - * @param date - The date to validate - * @returns true if date is valid - */ export const isValidDate = (date: string | number | Date | Dayjs | null | undefined): boolean => { if (date == null) return false return dayjs(date).isValid() } -/** - * Get the current date/time - * @returns Dayjs object representing current date/time - */ -export const getCurrentDate = (): Dayjs => { - return dayjs() -} - -/** - * Create a dayjs object from a date - * @param date - The date to parse (optional, defaults to current date) - * @param format - Optional format string for parsing - * @returns Dayjs object - */ export const createDate = ( date?: string | number | Date | Dayjs | null, format?: string, @@ -107,28 +75,20 @@ export const createDate = ( return dayjs(date) } -/** - * Format a date string - * @param date - The date to format - * @param format - The format pattern (default: 'YYYY-MM-DD') - * @returns Formatted date string - */ +export const parseDateStrict = (date: string, format: string): Dayjs | null => { + const parsed = dayjs(date, format, true) + return parsed.isValid() ? parsed : null +} + export const formatDate = ( date: string | number | Date | Dayjs | null | undefined, - format = 'YYYY-MM-DD', + format: string = DATE_FORMATS.DATE_ONLY, ): string => { if (date == null) return '' const d = dayjs(date) return d.isValid() ? d.format(format) : '' } -/** - * Add time to a date - * @param date - The date to add to - * @param amount - The amount to add - * @param unit - The unit of time (e.g., 'month', 'day', 'year') - * @returns New Dayjs object with added time - */ export const addDate = ( date: string | number | Date | Dayjs, amount: number, @@ -137,13 +97,6 @@ export const addDate = ( return dayjs(date).add(amount, unit) } -/** - * Subtract time from a date - * @param date - The date to subtract from - * @param amount - The amount to subtract - * @param unit - The unit of time (e.g., 'month', 'day', 'year') - * @returns New Dayjs object with subtracted time - */ export const subtractDate = ( date: string | number | Date | Dayjs, amount: number, @@ -151,32 +104,3 @@ export const subtractDate = ( ): Dayjs => { return dayjs(date).subtract(amount, unit) } - -/** - * Get the start of a unit of time - * @param date - The date - * @param unit - The unit of time (e.g., 'month', 'day', 'year') - * @returns Dayjs object at the start of the unit - */ -export const startOfDate = (date: string | number | Date | Dayjs, unit: OpUnitType): Dayjs => { - return dayjs(date).startOf(unit) -} - -/** - * Get the end of a unit of time - * @param date - The date - * @param unit - The unit of time (e.g., 'month', 'day', 'year') - * @returns Dayjs object at the end of the unit - */ -export const endOfDate = (date: string | number | Date | Dayjs, unit: OpUnitType): Dayjs => { - return dayjs(date).endOf(unit) -} - -/** - * Clone a date object - * @param date - The date to clone - * @returns New Dayjs object with the same value - */ -export const cloneDate = (date: string | number | Date | Dayjs): Dayjs => { - return dayjs(date).clone() -} diff --git a/admin-ui/config/webpack.config.client.dev.ts b/admin-ui/config/webpack.config.client.dev.ts index 8ee380670e..30ca42e9b7 100755 --- a/admin-ui/config/webpack.config.client.dev.ts +++ b/admin-ui/config/webpack.config.client.dev.ts @@ -59,7 +59,7 @@ const webpackConfig: WebpackConfig & { devServer?: DevServerConfig } = { priority: 17, }, utils: { - test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios|formik|yup)[\\/]/, + test: /[\\/]node_modules[\\/](lodash|dayjs|axios|formik|yup)[\\/]/, name: 'utils-vendor', chunks: 'all', priority: 16, diff --git a/admin-ui/config/webpack.config.client.prod.ts b/admin-ui/config/webpack.config.client.prod.ts index dbbb74e1c6..c94229707d 100755 --- a/admin-ui/config/webpack.config.client.prod.ts +++ b/admin-ui/config/webpack.config.client.prod.ts @@ -63,7 +63,7 @@ const webpackConfig: WebpackConfig & { devServer?: DevServerConfig } = { priority: 17, }, utils: { - test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios|formik|yup)[\\/]/, + test: /[\\/]node_modules[\\/](lodash|dayjs|axios|formik|yup)[\\/]/, name: 'utils-vendor', chunks: 'all', priority: 16, diff --git a/admin-ui/docs/CODE_SPLITTING.md b/admin-ui/docs/CODE_SPLITTING.md index d0d7656a71..7710471341 100644 --- a/admin-ui/docs/CODE_SPLITTING.md +++ b/admin-ui/docs/CODE_SPLITTING.md @@ -12,7 +12,7 @@ This document outlines the comprehensive code splitting strategy implemented in - **Material-UI**: `@mui/*`, `@emotion/*` - **Redux**: `@reduxjs/*`, `redux`, `redux-saga`, `redux-persist` - **Charts**: `recharts`, `react-ace`, `ace-builds` -- **Utilities**: `lodash`, `moment`, `dayjs`, `axios`, `formik`, `yup` +- **Utilities**: `lodash`, `dayjs`, `axios`, `formik`, `yup` #### Plugin-based Splitting diff --git a/admin-ui/package.json b/admin-ui/package.json index 63c8f154e8..dfe60e7e5d 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -168,7 +168,6 @@ "jszip": "^3.10.1", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", - "moment": "^2.29.4", "node-fetch": "^3.3.1", "openapi-merge-cli": "^1.3.2", "path-browserify": "^1.0.1", diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx index 070867d91a..b7552d9bf2 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx @@ -24,8 +24,8 @@ import { getAssetTypes, } from 'Plugins/admin/redux/features/AssetSlice' import customColors from '../../../../app/customColors' -import moment from 'moment' -import { Document, RootState, SearchEvent } from './types' +import { formatDate } from '@/utils/dayjsUtils' +import { Document, RootState } from './types' import { DeleteAssetSagaPayload } from 'Plugins/admin/redux/features/types' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' @@ -78,12 +78,13 @@ const JansAssetListPage: React.FC = () => { ) const handleOptionsChange = useCallback( - (event: SearchEvent) => { - if (event.target.name === 'limit') { - memoLimit = Number(event.target.value) - } else if (event.target.name === 'pattern') { - memoPattern = String(event.target.value) || undefined - if (event.keyCode === 13) { + (event: React.ChangeEvent | React.KeyboardEvent) => { + const target = event.target as HTMLInputElement + if (target.name === 'limit') { + memoLimit = Number(target.value) + } else if (target.name === 'pattern') { + memoPattern = String(target.value) || undefined + if ('keyCode' in event && event.keyCode === 13) { const newOptions = { limit: limit, pattern: memoPattern, @@ -305,7 +306,7 @@ const JansAssetListPage: React.FC = () => { field: 'creationDate', render: (rowData: Document) => (
- {moment(rowData.creationDate).format('YYYY-MM-DD')} + {rowData.creationDate ? formatDate(rowData.creationDate, 'YYYY-MM-DD') : ''}
), }, diff --git a/admin-ui/plugins/auth-server/components/Clients/ClientActiveTokens.js b/admin-ui/plugins/auth-server/components/Clients/ClientActiveTokens.js index 6b44bef289..b9cf355688 100644 --- a/admin-ui/plugins/auth-server/components/Clients/ClientActiveTokens.js +++ b/admin-ui/plugins/auth-server/components/Clients/ClientActiveTokens.js @@ -13,11 +13,10 @@ import { Box, Grid, MenuItem, Paper, TablePagination, TextField, Tooltip } from import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import getThemeColor from 'Context/theme/config' -import moment from 'moment' import { deleteClientToken, getTokenByClient } from '../../redux/features/oidcSlice' import ClientActiveTokenDetailPage from './ClientActiveTokenDetailPage' import { Button } from 'Components' -import dayjs from 'dayjs' +import { formatDate, diffDate, createDate } from '@/utils/dayjsUtils' import PropTypes from 'prop-types' import { Button as MaterialButton } from '@mui/material' import FilterListIcon from '@mui/icons-material/FilterList' @@ -71,8 +70,8 @@ function ClientActiveTokens({ client }) { setPageNumber(page) let conditionquery = `clnId=${client.inum}` if (pattern.dateAfter && pattern.dateBefore) { - conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}` - conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}` + conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}` + conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}` } getTokens(startCount, limit, conditionquery) @@ -114,8 +113,8 @@ function ClientActiveTokens({ client }) { const startCount = pageNumber * limit let conditionquery = `clnId=${client.inum}` if (pattern.dateAfter && pattern.dateBefore) { - conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}` - conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}` + conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}` + conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}` } getTokens(startCount, limit, conditionquery) } @@ -132,8 +131,8 @@ function ClientActiveTokens({ client }) { const startCount = pageNumber * limit let conditionquery = `clnId=${client.inum}` if (pattern.dateAfter && pattern.dateBefore) { - conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}` - conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}` + conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}` + conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}` } getTokens(startCount, limit, conditionquery) } @@ -202,6 +201,13 @@ function ClientActiveTokens({ client }) { const result = updatedToken?.items?.length ? updatedToken.items .map((item) => { + const expirationDate = item.expirationDate + ? formatDate(item.expirationDate, 'YYYY/DD/MM HH:mm:ss') + : '' + const creationDate = item.creationDate + ? formatDate(item.creationDate, 'YYYY/DD/MM HH:mm:ss') + : '' + return { id: item.tokenCode, tokenCode: item.tokenCode, @@ -210,15 +216,16 @@ function ClientActiveTokens({ client }) { deletable: item.deletable, attributes: item.attributes, grantType: item.grantType, - expirationDate: moment(item.expirationDate).format('YYYY/DD/MM HH:mm:ss'), - creationDate: moment(item.creationDate).format('YYYY/DD/MM HH:mm:ss'), + expirationDate, + creationDate, } }) - .sort((a, b) => { - return moment(b.creationDate, 'YYYY/DD/MM HH:mm:ss').diff( - moment(a.creationDate, 'YYYY/DD/MM HH:mm:ss'), - ) - }) + .sort((a, b) => + diffDate( + createDate(b.creationDate, 'YYYY/DD/MM HH:mm:ss'), + createDate(a.creationDate, 'YYYY/DD/MM HH:mm:ss'), + ), + ) : [] setData(result) } else if (!updatedToken || (updatedToken && !updatedToken.items)) { diff --git a/admin-ui/plugins/auth-server/components/Clients/ClientCibaParUmaPanel.js b/admin-ui/plugins/auth-server/components/Clients/ClientCibaParUmaPanel.js index 30adc591ec..14eab8a04a 100644 --- a/admin-ui/plugins/auth-server/components/Clients/ClientCibaParUmaPanel.js +++ b/admin-ui/plugins/auth-server/components/Clients/ClientCibaParUmaPanel.js @@ -5,7 +5,7 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap' import isEmpty from 'lodash/isEmpty' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import moment from 'moment' +import { formatDate } from '@/utils/dayjsUtils' import AceEditor from 'react-ace' import { Card, Col, Container, FormGroup } from 'Components' import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' @@ -416,7 +416,9 @@ function ClientCibaParUmaPanel({ - {moment(selectedUMA?.creationDate).format('ddd, MMM DD, YYYY h:mm:ss A')} + {selectedUMA?.creationDate + ? formatDate(selectedUMA.creationDate, 'ddd, MMM DD, YYYY h:mm:ss A') + : ''} diff --git a/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkItem.tsx b/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkItem.tsx index b887a152a1..b52e4b1d42 100644 --- a/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkItem.tsx +++ b/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkItem.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Col, FormGroup, Input, Card, CardBody, Accordion } from 'Components' -import moment from 'moment' +import { formatDate } from '@/utils/dayjsUtils' import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' import customColors from '@/customColors' import type { JwkItemProps } from '../types' @@ -56,7 +56,7 @@ const JwkItem = React.memo(function JwkItem({ item, index }: JwkItemProps): Reac data-testid="exp" name="exp" readOnly - defaultValue={item.exp != null ? moment(item.exp).format(DATE_FORMAT) : ''} + defaultValue={item.exp != null ? formatDate(item.exp, DATE_FORMAT) : ''} /> diff --git a/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkListPage.test.tsx b/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkListPage.test.tsx index f09a7f4cac..7d33021551 100644 --- a/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkListPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Configuration/Keys/Jwks/JwkListPage.test.tsx @@ -4,10 +4,11 @@ import JwkListPage from './JwkListPage' import { Provider } from 'react-redux' import i18n from '../../../../../../app/i18n' import { I18nextProvider } from 'react-i18next' -import moment from 'moment' +import { formatDate } from '../../../../../../app/utils/dayjsUtils' import { combineReducers, configureStore } from '@reduxjs/toolkit' import { mockJwksConfig } from '../__fixtures__/jwkTestData' import { DATE_FORMAT } from '../constants' +import { useJwkApi } from '../hooks' jest.mock('../hooks', () => ({ useJwkApi: jest.fn(() => ({ @@ -45,14 +46,13 @@ describe('JwkListPage', () => { expect(screen.getByTestId('kty')).toHaveValue(firstKey?.kty ?? '') expect(screen.getByTestId('use')).toHaveValue(firstKey?.use ?? '') expect(screen.getByTestId('alg')).toHaveValue(firstKey?.alg ?? '') - if (firstKey?.exp != null) { - expect(screen.getByTestId('exp')).toHaveValue(moment(firstKey.exp).format(DATE_FORMAT)) - } + const expectedExp = firstKey?.exp != null ? formatDate(firstKey.exp, DATE_FORMAT) : '' + expect(screen.getByTestId('exp')).toHaveValue(expectedExp) }) it('should handle undefined exp gracefully', () => { - const { useJwkApi } = require('../hooks') - useJwkApi.mockReturnValue({ + const mockedUseJwkApi = useJwkApi as jest.Mock + mockedUseJwkApi.mockReturnValue({ jwks: { keys: [{ ...mockJwksConfig.keys[0], exp: undefined }], }, @@ -67,8 +67,8 @@ describe('JwkListPage', () => { }) it('should handle null exp gracefully', () => { - const { useJwkApi } = require('../hooks') - useJwkApi.mockReturnValue({ + const mockedUseJwkApi = useJwkApi as jest.Mock + mockedUseJwkApi.mockReturnValue({ jwks: { keys: [{ ...mockJwksConfig.keys[0], exp: null }], }, diff --git a/admin-ui/plugins/auth-server/components/Configuration/Keys/KeysPage.test.tsx b/admin-ui/plugins/auth-server/components/Configuration/Keys/KeysPage.test.tsx index 8e38b02b69..db9baeb69d 100644 --- a/admin-ui/plugins/auth-server/components/Configuration/Keys/KeysPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Configuration/Keys/KeysPage.test.tsx @@ -4,7 +4,7 @@ import KeysPage from './KeysPage' import { Provider } from 'react-redux' import i18n from '../../../../../app/i18n' import { I18nextProvider } from 'react-i18next' -import moment from 'moment' +import { formatDate } from '@/utils/dayjsUtils' import { combineReducers, configureStore } from '@reduxjs/toolkit' import { mockJwksConfig, @@ -50,7 +50,7 @@ describe('KeysPage', () => { expect(screen.getByTestId('use')).toHaveValue(firstKey?.use ?? '') expect(screen.getByTestId('alg')).toHaveValue(firstKey?.alg ?? '') if (firstKey?.exp != null) { - expect(screen.getByTestId('exp')).toHaveValue(moment(firstKey.exp).format(DATE_FORMAT)) + expect(screen.getByTestId('exp')).toHaveValue(formatDate(firstKey.exp, DATE_FORMAT)) } }) diff --git a/admin-ui/plugins/auth-server/components/Scopes/ScopeForm.tsx b/admin-ui/plugins/auth-server/components/Scopes/ScopeForm.tsx index 9dab03dede..1d53cffe2a 100644 --- a/admin-ui/plugins/auth-server/components/Scopes/ScopeForm.tsx +++ b/admin-ui/plugins/auth-server/components/Scopes/ScopeForm.tsx @@ -26,7 +26,7 @@ import { SCOPE } from 'Utils/ApiResources' import { useTranslation } from 'react-i18next' import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' -import moment from 'moment' +import { formatDate, DATE_FORMATS } from '@/utils/dayjsUtils' import { adminUiFeatures } from 'Plugins/admin/helper/utils' import customColors from '@/customColors' import type { ScopeFormProps, ScopeFormValues, ScopeClient, ExtendedScope } from './types' @@ -605,7 +605,10 @@ const ScopeForm: React.FC = ({ @@ -672,7 +675,10 @@ const ScopeForm: React.FC = ({ diff --git a/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx b/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx index df058fc0a1..5579f9a57c 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx @@ -6,7 +6,6 @@ import React, { useMemo, type SyntheticEvent, } from 'react' -import moment from 'moment' import isEmpty from 'lodash/isEmpty' import MaterialTable, { type Action } from '@material-table/core' import Autocomplete from '@mui/material/Autocomplete' @@ -39,7 +38,8 @@ import SessionDetailPage from '../Sessions/SessionDetailPage' import { useCedarling } from '@/cedarling' import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import dayjs, { Dayjs } from 'dayjs' +import { formatDate } from '@/utils/dayjsUtils' +import { Dayjs } from 'dayjs' import FilterListIcon from '@mui/icons-material/FilterList' import GetAppIcon from '@mui/icons-material/GetApp' import ViewColumnIcon from '@mui/icons-material/ViewColumn' @@ -249,7 +249,11 @@ const SessionListPage: React.FC = () => { title: `${t('fields.auth_time')}`, field: 'authenticationTime', render: (rowData: Session) => ( - {moment(rowData.authenticationTime).format('ddd, MMM DD, YYYY h:mm:ss A')} + + {rowData.authenticationTime + ? formatDate(rowData.authenticationTime, 'ddd, MMM DD, YYYY h:mm:ss A') + : ''} + ), }, { @@ -354,16 +358,16 @@ const SessionListPage: React.FC = () => { const header = keys.map((item) => item.replaceAll('-', ' ').toUpperCase()).join(',') - const updateData = data.map((row) => { - return { - [t('fields.username')]: row.sessionAttributes.auth_user, - [t('fields.ip_address')]: row.sessionAttributes.remote_ip, - [t('fields.client_id_used')]: row.sessionAttributes.client_id, - [t('fields.auth_time')]: moment(row.authenticationTime).format('YYYY-MM-DD h:mm:ss A'), - [t('fields.acr')]: row.sessionAttributes.acr_values, - [t('fields.state')]: row.state, - } - }) + const updateData = data.map((row) => ({ + [t('fields.username')]: row.sessionAttributes.auth_user, + [t('fields.ip_address')]: row.sessionAttributes.remote_ip, + [t('fields.client_id_used')]: row.sessionAttributes.client_id, + [t('fields.auth_time')]: row.authenticationTime + ? formatDate(row.authenticationTime, 'YYYY-MM-DD h:mm:ss A') + : '', + [t('fields.acr')]: row.sessionAttributes.acr_values, + [t('fields.state')]: row.state, + })) const rows = updateData.map((row) => { return keys.map((key) => row[key]).join(',') @@ -412,7 +416,7 @@ const SessionListPage: React.FC = () => { const searchValue = searchFilter !== 'expirationDate' && searchFilter !== 'authenticationTime' ? pattern - : dayjs(date).format('YYYY-MM-DD') + : formatDate(date, 'YYYY-MM-DD') const isSessionAttribute = searchFilter === 'client_id' || searchFilter === 'auth_user' diff --git a/admin-ui/plugins/saml/components/WebsiteSsoIdentityProviderForm.tsx b/admin-ui/plugins/saml/components/WebsiteSsoIdentityProviderForm.tsx index dad5ece11d..87da8a286e 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoIdentityProviderForm.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoIdentityProviderForm.tsx @@ -273,9 +273,9 @@ const WebsiteSsoIdentityProviderForm = ({ const value = formik.values[fieldName] return Boolean( error && - (touched || - formik.submitCount > 0 || - (value !== undefined && value !== null && String(value).length > 0)), + (touched || + formik.submitCount > 0 || + (value !== undefined && value !== null && String(value).length > 0)), ) }, [formik.errors, formik.touched, formik.values, formik.submitCount], @@ -412,7 +412,7 @@ const WebsiteSsoIdentityProviderForm = ({ rsize={8} showError={Boolean( formik.errors.idpEntityId && - (formik.touched.idpEntityId || formik.submitCount > 0), + (formik.touched.idpEntityId || formik.submitCount > 0), )} errorMessage={formik.errors.idpEntityId} disabled={viewOnly} @@ -432,7 +432,7 @@ const WebsiteSsoIdentityProviderForm = ({ required={!formik.values.metaDataFileImportedFlag} showError={Boolean( formik.errors.nameIDPolicyFormat && - (formik.touched.nameIDPolicyFormat || formik.submitCount > 0), + (formik.touched.nameIDPolicyFormat || formik.submitCount > 0), )} errorMessage={formik.errors.nameIDPolicyFormat} disabled={viewOnly} @@ -451,7 +451,7 @@ const WebsiteSsoIdentityProviderForm = ({ rsize={8} showError={Boolean( formik.errors.singleSignOnServiceUrl && - (formik.touched.singleSignOnServiceUrl || formik.submitCount > 0), + (formik.touched.singleSignOnServiceUrl || formik.submitCount > 0), )} errorMessage={formik.errors.singleSignOnServiceUrl} disabled={viewOnly} @@ -468,7 +468,7 @@ const WebsiteSsoIdentityProviderForm = ({ rsize={8} showError={Boolean( formik.errors.singleLogoutServiceUrl && - (formik.touched.singleLogoutServiceUrl || formik.submitCount > 0), + (formik.touched.singleLogoutServiceUrl || formik.submitCount > 0), )} errorMessage={formik.errors.singleLogoutServiceUrl} disabled={viewOnly} @@ -486,7 +486,7 @@ const WebsiteSsoIdentityProviderForm = ({ type="textarea" showError={Boolean( formik.errors.signingCertificate && - (formik.touched.signingCertificate || formik.submitCount > 0), + (formik.touched.signingCertificate || formik.submitCount > 0), )} errorMessage={formik.errors.signingCertificate} disabled={viewOnly} @@ -505,7 +505,7 @@ const WebsiteSsoIdentityProviderForm = ({ type="textarea" showError={Boolean( formik.errors.encryptionPublicKey && - (formik.touched.encryptionPublicKey || formik.submitCount > 0), + (formik.touched.encryptionPublicKey || formik.submitCount > 0), )} errorMessage={formik.errors.encryptionPublicKey} disabled={viewOnly} @@ -523,7 +523,7 @@ const WebsiteSsoIdentityProviderForm = ({ rsize={8} showError={Boolean( formik.errors.principalAttribute && - (formik.touched.principalAttribute || formik.submitCount > 0), + (formik.touched.principalAttribute || formik.submitCount > 0), )} errorMessage={formik.errors.principalAttribute} disabled={viewOnly} @@ -540,7 +540,7 @@ const WebsiteSsoIdentityProviderForm = ({ rsize={8} showError={Boolean( formik.errors.principalType && - (formik.touched.principalType || formik.submitCount > 0), + (formik.touched.principalType || formik.submitCount > 0), )} errorMessage={formik.errors.principalType} disabled={viewOnly} diff --git a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx index a0699fb885..9f0499c24e 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx @@ -313,7 +313,7 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.displayName && - (formik.touched.displayName || formik.submitCount > 0), + (formik.touched.displayName || formik.submitCount > 0), )} errorMessage={formik.errors.displayName} disabled={viewOnly} @@ -352,7 +352,7 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.spLogoutURL && - (formik.touched.spLogoutURL || formik.submitCount > 0), + (formik.touched.spLogoutURL || formik.submitCount > 0), )} errorMessage={formik.errors.spLogoutURL} disabled={viewOnly} @@ -457,8 +457,8 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.samlMetadata?.singleLogoutServiceUrl && - (formik.touched.samlMetadata?.singleLogoutServiceUrl || - formik.submitCount > 0), + (formik.touched.samlMetadata?.singleLogoutServiceUrl || + formik.submitCount > 0), )} errorMessage={formik.errors.samlMetadata?.singleLogoutServiceUrl} disabled={viewOnly} @@ -479,7 +479,7 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.samlMetadata?.entityId && - (formik.touched.samlMetadata?.entityId || formik.submitCount > 0), + (formik.touched.samlMetadata?.entityId || formik.submitCount > 0), )} errorMessage={formik.errors.samlMetadata?.entityId} disabled={viewOnly} @@ -501,7 +501,8 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.samlMetadata?.nameIDPolicyFormat && - (formik.touched.samlMetadata?.nameIDPolicyFormat || formik.submitCount > 0), + (formik.touched.samlMetadata?.nameIDPolicyFormat || + formik.submitCount > 0), )} errorMessage={formik.errors.samlMetadata?.nameIDPolicyFormat} disabled={viewOnly} @@ -522,8 +523,8 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.samlMetadata?.jansAssertionConsumerServiceGetURL && - (formik.touched.samlMetadata?.jansAssertionConsumerServiceGetURL || - formik.submitCount > 0), + (formik.touched.samlMetadata?.jansAssertionConsumerServiceGetURL || + formik.submitCount > 0), )} errorMessage={formik.errors.samlMetadata?.jansAssertionConsumerServiceGetURL} disabled={viewOnly} @@ -544,8 +545,8 @@ const WebsiteSsoServiceProviderForm = ({ rsize={8} showError={Boolean( formik.errors.samlMetadata?.jansAssertionConsumerServicePostURL && - (formik.touched.samlMetadata?.jansAssertionConsumerServicePostURL || - formik.submitCount > 0), + (formik.touched.samlMetadata?.jansAssertionConsumerServicePostURL || + formik.submitCount > 0), )} errorMessage={formik.errors.samlMetadata?.jansAssertionConsumerServicePostURL} disabled={viewOnly} diff --git a/admin-ui/plugins/user-management/components/User2FADevicesModal.tsx b/admin-ui/plugins/user-management/components/User2FADevicesModal.tsx index 4473f97e9d..2eec92f583 100644 --- a/admin-ui/plugins/user-management/components/User2FADevicesModal.tsx +++ b/admin-ui/plugins/user-management/components/User2FADevicesModal.tsx @@ -15,7 +15,7 @@ import { } from 'JansConfigApi' import { useQueryClient } from '@tanstack/react-query' import { updateToast } from 'Redux/features/toastSlice' -import moment from 'moment' +import { formatDate } from '@/utils/dayjsUtils' import customColors from '@/customColors' import UserDeviceDetailViewPage from './UserDeviceDetailViewPage' import { @@ -127,9 +127,7 @@ const User2FADevicesModal = ({ isOpen, onClose, userDetails, theme }: User2FADev modality: item?.soft ? 'Soft token - time based (totp)' : 'Hard token - time based (totp)', - dateAdded: moment(new Date((item.addedOn || 0) * 1000).toString()).format( - 'YYYY-MM-DD HH:mm:ss', - ), + dateAdded: formatDate((item.addedOn || 0) * 1000, 'YYYY-MM-DD HH:mm:ss'), type: 'OTP', } }) || [] @@ -152,9 +150,7 @@ const User2FADevicesModal = ({ isOpen, onClose, userDetails, theme }: User2FADev id: item.id, nickName: displayName, modality: modality, - dateAdded: item.creationDate - ? moment(item.creationDate).format('YYYY-MM-DD HH:mm:ss') - : '-', + dateAdded: item.creationDate ? formatDate(item.creationDate, 'YYYY-MM-DD HH:mm:ss') : '-', type: deviceType, registrationData: item.registrationData, deviceData: item.deviceData, diff --git a/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx b/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx index 345881ff5e..83f40b7529 100644 --- a/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx +++ b/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react' import { Row, Col } from 'Components' import GluuFormDetailRow from 'Routes/Apps/Gluu/GluuFormDetailRow' -import moment from 'moment' +import { formatDate, parseDateStrict } from '@/utils/dayjsUtils' import DOMPurify from 'dompurify' import customColors from '@/customColors' import { BIRTHDATE_ATTR } from '../common/Constants' @@ -63,8 +63,14 @@ const UserDetailViewPage = ({ row }: RowProps) => { {rowData.customAttributes?.map((data: CustomObjectAttribute, key: number) => { let valueToShow = '' if (data.name === BIRTHDATE_ATTR) { - const m = moment(data?.values?.[0], 'YYYY-MM-DD', true) - valueToShow = m.isValid() ? m.format('YYYY-MM-DD') : '' + const raw = data?.values?.[0] + const birthdatePattern = /^\d{4}-\d{2}-\d{2}$/ + if (typeof raw === 'string' && birthdatePattern.test(raw)) { + const parsed = parseDateStrict(raw, 'YYYY-MM-DD') + valueToShow = parsed ? formatDate(parsed, 'YYYY-MM-DD') : '' + } else { + valueToShow = '' + } } else { valueToShow = data.multiValued ? data?.values?.join(', ') || '' @@ -77,15 +83,17 @@ const UserDetailViewPage = ({ row }: RowProps) => { ) : null} diff --git a/admin-ui/plugins/user-management/helper/validations.ts b/admin-ui/plugins/user-management/helper/validations.ts index 3209623e09..bcc0cd1e0b 100644 --- a/admin-ui/plugins/user-management/helper/validations.ts +++ b/admin-ui/plugins/user-management/helper/validations.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup' -import moment from 'moment/moment' +import { formatDate, isValidDate } from '@/utils/dayjsUtils' import { CustomUser, PersonAttribute } from '../types/UserApiTypes' import { UserEditFormValues } from '../types/ComponentTypes' import { CustomObjectAttribute } from 'JansConfigApi' @@ -117,13 +117,14 @@ const processBirthdateAttribute = ( const attrSingleValue = customAttr.value if (!customAttr.name) return - const dateSource = attrValues.length > 0 ? attrValues[0] : attrSingleValue + const rawDate = + attrValues.length > 0 + ? (attrValues[0] as unknown as string | number | Date | null) + : (attrSingleValue as unknown as string | number | Date | null) - if (dateSource !== undefined && dateSource !== null) { - const parsedDate = moment(dateSource) - - if (parsedDate.isValid()) { - initialValues[customAttr.name] = parsedDate.format('YYYY-MM-DD') + if (rawDate !== undefined && rawDate !== null) { + if (isValidDate(rawDate)) { + initialValues[customAttr.name] = formatDate(rawDate, 'YYYY-MM-DD') return } } diff --git a/admin-ui/plugins/user-management/utils/attributeTransformUtils.ts b/admin-ui/plugins/user-management/utils/attributeTransformUtils.ts index ffe58e2277..2b471f192a 100644 --- a/admin-ui/plugins/user-management/utils/attributeTransformUtils.ts +++ b/admin-ui/plugins/user-management/utils/attributeTransformUtils.ts @@ -1,4 +1,4 @@ -import moment from 'moment' +import { parseDateStrict } from '@/utils/dayjsUtils' import { CustomObjectAttribute } from 'JansConfigApi' import { BIRTHDATE_ATTR, JANS_ADMIN_UI_ROLE_ATTR } from '../common/Constants' import { @@ -46,8 +46,13 @@ export const normalizeSingleValue = (value: FormValueEntry, attributeName: strin : '' if (attributeName === BIRTHDATE_ATTR && normalized) { - const m = moment(normalized, 'YYYY-MM-DD', true) - return m.isValid() ? m.format('YYYY-MM-DD') : '' + // Ensure the value strictly matches YYYY-MM-DD and is a valid date + const birthdatePattern = /^\d{4}-\d{2}-\d{2}$/ + if (!birthdatePattern.test(normalized)) { + return '' + } + const parsed = parseDateStrict(normalized, 'YYYY-MM-DD') + return parsed ? parsed.format('YYYY-MM-DD') : '' } return normalized } diff --git a/admin-ui/plugins/user-management/utils/userFormUtils.ts b/admin-ui/plugins/user-management/utils/userFormUtils.ts index 2ebe90d873..6ec91c864d 100644 --- a/admin-ui/plugins/user-management/utils/userFormUtils.ts +++ b/admin-ui/plugins/user-management/utils/userFormUtils.ts @@ -1,4 +1,4 @@ -import moment from 'moment/moment' +import { formatDate, isValidDate } from '@/utils/dayjsUtils' import { CustomObjectAttribute, PagedResultEntriesItem } from 'JansConfigApi' import { BIRTHDATE_ATTR, USER_PASSWORD_ATTR } from '../common/Constants' @@ -31,10 +31,13 @@ const processBirthdateAttribute = ( ) => { const attrValues = customAttr.values ?? [] const attrSingleValue = customAttr.value - const dateSource = - attrValues.length > 0 ? JSON.stringify(attrValues[0]) : JSON.stringify(attrSingleValue) - if (dateSource) { - initialValues[customAttr.name || ''] = moment(dateSource).format('YYYY-MM-DD') + const rawDate = + attrValues.length > 0 + ? (attrValues[0] as unknown as string | number | Date | null) + : (attrSingleValue as unknown as string | number | Date | null) + + if (rawDate !== undefined && rawDate !== null && customAttr.name) { + initialValues[customAttr.name] = isValidDate(rawDate) ? formatDate(rawDate, 'YYYY-MM-DD') : '' } }