From 07d30b650ae2cde896c93690fa8a1948b6315bdf Mon Sep 17 00:00:00 2001 From: Palash Thakur <117917450+palasht75@users.noreply.github.com> Date: Thu, 28 May 2026 01:59:43 -0400 Subject: [PATCH] Format Terms and Conditions accepted timestamp (#49031) Closes #44591 Signed-off-by: Palash Thakur Co-authored-by: Palash Thakur --- js/apps/admin-ui/src/user/UserForm.tsx | 37 +++++++++++- js/apps/admin-ui/test/users/main.spec.ts | 73 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/js/apps/admin-ui/src/user/UserForm.tsx b/js/apps/admin-ui/src/user/UserForm.tsx index afea054e398..39a9a89543c 100644 --- a/js/apps/admin-ui/src/user/UserForm.tsx +++ b/js/apps/admin-ui/src/user/UserForm.tsx @@ -47,6 +47,8 @@ import { useNavigate } from "react-router-dom"; import { CopyToClipboardButton } from "../components/copy-to-clipboard-button/CopyToClipboardButton"; import { GroupResourceContext } from "../context/group-resource/GroupResourceContext"; +const TERMS_AND_CONDITIONS_ATTRIBUTE = "terms_and_conditions"; + export type BruteForced = { isBruteForceProtected?: boolean; isLocked?: boolean; @@ -86,6 +88,10 @@ export const UserForm = ({ const canViewFederationLink = hasAccess("view-realm"); const { whoAmI } = useWhoAmI(); + const termsAndConditionsAcceptedDate = toTermsAndConditionsAcceptedDate( + user?.attributes?.[TERMS_AND_CONDITIONS_ATTRIBUTE], + ); + const { handleSubmit, setValue, control, reset, formState } = form; const { errors } = formState; @@ -270,6 +276,19 @@ export const UserForm = ({ label={t("emailVerified")} labelIcon={t("emailVerifiedHelp")} /> + {termsAndConditionsAcceptedDate && ( + + + {formatDate(termsAndConditionsAcceptedDate)} + + + )} {user?.attributes?.["kc.email.pending"] && ( { - return attribute.name !== "kc.email.pending"; + return ( + attribute.name !== "kc.email.pending" && + (attribute.name !== TERMS_AND_CONDITIONS_ATTRIBUTE || + !termsAndConditionsAcceptedDate) + ); }, ), }} @@ -430,3 +453,15 @@ export const UserForm = ({ ); }; + +function toTermsAndConditionsAcceptedDate(value: unknown): Date | undefined { + const timestamp = Number(Array.isArray(value) ? value[0] : value); + + if (!Number.isFinite(timestamp) || timestamp <= 0) { + return undefined; + } + + const date = new Date(timestamp * 1000); + + return Number.isNaN(date.getTime()) ? undefined : date; +} diff --git a/js/apps/admin-ui/test/users/main.spec.ts b/js/apps/admin-ui/test/users/main.spec.ts index 706269518bb..7f4ecdcccc5 100644 --- a/js/apps/admin-ui/test/users/main.spec.ts +++ b/js/apps/admin-ui/test/users/main.spec.ts @@ -160,6 +160,79 @@ test.describe("Existing users", () => { await assertNotificationMessage(page, "The user has been saved"); }); + test("formats Terms and Conditions accepted timestamp", async ({ page }) => { + const termsAcceptedTimestamp = "1700000000"; + const email = "existing-user@example.com"; + const firstName = "Existing"; + const lastName = "User"; + + await using testBed = await createTestBed({ + attributes: { + userProfileEnabled: "true", + }, + requiredActions: [ + { + alias: "TERMS_AND_CONDITIONS", + name: "Terms and Conditions", + providerId: "TERMS_AND_CONDITIONS", + enabled: true, + defaultAction: false, + priority: 20, + config: {}, + }, + ], + users: [ + { + username: existingUserName, + email, + firstName, + lastName, + attributes: { + terms_and_conditions: [termsAcceptedTimestamp], + }, + }, + ], + }); + const user = await adminClient.findUserByUsername( + testBed.realm, + existingUserName, + ); + + await login(page, { + to: toUser({ realm: testBed.realm, id: user.id!, tab: "settings" }), + }); + + const termsAcceptedField = page.getByTestId("terms_and_conditions"); + await expect(termsAcceptedField).toBeVisible(); + await expect(termsAcceptedField).not.toHaveText(termsAcceptedTimestamp); + await expect( + page.getByText(termsAcceptedTimestamp, { exact: true }), + ).toHaveCount(0); + await expect( + page.locator(`input[value="${termsAcceptedTimestamp}"]`), + ).toHaveCount(0); + + const displayedTimestamp = await termsAcceptedField.innerText(); + expect(displayedTimestamp).toContain("2023"); + expect(displayedTimestamp).toMatch(/\D/); + expect(displayedTimestamp).toMatch(/\d{1,2}:\d{2}/); + + await expect(page.getByTestId("email")).toHaveValue(email); + await expect(page.getByTestId("firstName")).toHaveValue(firstName); + await expect(page.getByTestId("lastName")).toHaveValue(lastName); + + await clickSaveButton(page); + await assertNotificationMessage(page, "The user has been saved"); + + const updatedUser = await adminClient.findUserByUsername( + testBed.realm, + existingUserName, + ); + expect(updatedUser.attributes?.terms_and_conditions).toEqual([ + termsAcceptedTimestamp, + ]); + }); + test("shows validation error for empty required attribute", async ({ page, }) => {