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,
}) => {