From 22496e36eb178b2a0098b20f084911df43953321 Mon Sep 17 00:00:00 2001
From: giorgoslytos
Date: Wed, 7 Feb 2024 15:18:27 +0200
Subject: [PATCH 1/7] feat: Addition of Sessions page
---
src/account/Fallback.tsx | 3 +
src/account/TemplateProps.ts | 9 ++-
src/account/kcContext/KcContext.ts | 25 +++++++-
src/account/kcContext/kcContextMocks.ts | 28 +++++++++
src/account/lib/useGetClassName.ts | 1 +
src/account/pages/Sessions.tsx | 68 ++++++++++++++++++++++
src/bin/keycloakify/generateFtl/pageId.ts | 2 +-
stories/account/pages/Sessions.stories.tsx | 32 ++++++++++
8 files changed, 165 insertions(+), 3 deletions(-)
create mode 100644 src/account/pages/Sessions.tsx
create mode 100644 stories/account/pages/Sessions.stories.tsx
diff --git a/src/account/Fallback.tsx b/src/account/Fallback.tsx
index 50e9225a..35a16f49 100644
--- a/src/account/Fallback.tsx
+++ b/src/account/Fallback.tsx
@@ -6,6 +6,7 @@ import { assert, type Equals } from "tsafe/assert";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
+const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
export default function Fallback(props: PageProps) {
const { kcContext, ...rest } = props;
@@ -16,6 +17,8 @@ export default function Fallback(props: PageProps) {
switch (kcContext.pageId) {
case "password.ftl":
return ;
+ case "sessions.ftl":
+ return ;
case "account.ftl":
return ;
}
diff --git a/src/account/TemplateProps.ts b/src/account/TemplateProps.ts
index 10ebf9aa..1f20c599 100644
--- a/src/account/TemplateProps.ts
+++ b/src/account/TemplateProps.ts
@@ -11,4 +11,11 @@ export type TemplateProps({
+ ...kcContextCommonMock,
+ "pageId": "sessions.ftl",
+ sessions: {
+ sessions: [
+ {
+ ...kcContextCommonMock.sessions,
+ ipAddress: "127.0.0.1",
+ started: new Date().toString(),
+ lastAccess: new Date().toString(),
+ expires: new Date().toString(),
+ clients: ["Chrome", "Firefox"]
+ }
+ ]
+ },
+ "stateChecker": ""
})
];
diff --git a/src/account/lib/useGetClassName.ts b/src/account/lib/useGetClassName.ts
index 51366ff0..f0ee87af 100644
--- a/src/account/lib/useGetClassName.ts
+++ b/src/account/lib/useGetClassName.ts
@@ -6,6 +6,7 @@ export const { useGetClassName } = createUseClassName({
"kcHtmlClass": undefined,
"kcBodyClass": undefined,
"kcButtonClass": "btn",
+ "kcContentWrapperClass": "row",
"kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg",
"kcButtonDefaultClass": "btn-default"
diff --git a/src/account/pages/Sessions.tsx b/src/account/pages/Sessions.tsx
new file mode 100644
index 00000000..1bb46355
--- /dev/null
+++ b/src/account/pages/Sessions.tsx
@@ -0,0 +1,68 @@
+import { clsx } from "keycloakify/tools/clsx";
+import type { PageProps } from "keycloakify/account/pages/PageProps";
+import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function Sessions(props: PageProps, I18n>) {
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+
+ const { getClassName } = useGetClassName({
+ doUseDefaultCss,
+ classes
+ });
+
+ console.log({ kcContext });
+ const { url, stateChecker, sessions } = kcContext;
+
+ const { msg } = i18n;
+ console.log({ sdf: kcContext.locale?.supported });
+ console.log({ asdf: "asdf" });
+ return (
+
+
+
+
{msg("sessionsHtmlTitle")}
+
+
+
+
+
+
+ {msg("ip")} |
+ {msg("started")} |
+ {msg("lastAccess")} |
+ {msg("expires")} |
+ {msg("clients")} |
+
+
+
+
+ {sessions.sessions.map((session, index: number) => (
+
+ {session.ipAddress} |
+ {session?.started} |
+ {session?.lastAccess} |
+ {session?.expires} |
+
+ {session.clients.map((client: string, clientIndex: number) => (
+
+ {client}
+
+
+ ))}
+ |
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/bin/keycloakify/generateFtl/pageId.ts b/src/bin/keycloakify/generateFtl/pageId.ts
index da3525f6..70062ac0 100644
--- a/src/bin/keycloakify/generateFtl/pageId.ts
+++ b/src/bin/keycloakify/generateFtl/pageId.ts
@@ -27,7 +27,7 @@ export const loginThemePageIds = [
"saml-post-form.ftl"
] as const;
-export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
+export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];
diff --git a/stories/account/pages/Sessions.stories.tsx b/stories/account/pages/Sessions.stories.tsx
new file mode 100644
index 00000000..b5d882aa
--- /dev/null
+++ b/stories/account/pages/Sessions.stories.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import type { ComponentMeta } from "@storybook/react";
+import { createPageStory } from "../createPageStory";
+
+const pageId = "sessions.ftl";
+
+const { PageStory } = createPageStory({ pageId });
+
+const meta: ComponentMeta = {
+ title: `account/${pageId}`,
+ component: PageStory,
+ parameters: {
+ viewMode: "story",
+ previewTabs: {
+ "storybook/docs/panel": {
+ hidden: true
+ }
+ }
+ }
+};
+
+export default meta;
+
+export const Default = () => ;
+
+export const WithMessage = () => (
+
+);
From 319d7dbe94ef840b8291006ac4a45a233e34d834 Mon Sep 17 00:00:00 2001
From: giorgoslytos
Date: Fri, 16 Feb 2024 17:40:12 +0200
Subject: [PATCH 2/7] feat: Addition of Totp account page
---
src/account/Fallback.tsx | 3 +
src/account/kcContext/KcContext.ts | 45 ++++-
src/account/kcContext/kcContextMocks.ts | 22 +++
src/account/pages/Totp.tsx | 186 +++++++++++++++++++++
src/bin/keycloakify/generateFtl/pageId.ts | 2 +-
stories/account/pages/Sessions.stories.tsx | 8 +-
stories/account/pages/Totp.stories.tsx | 51 ++++++
7 files changed, 308 insertions(+), 9 deletions(-)
create mode 100644 src/account/pages/Totp.tsx
create mode 100644 stories/account/pages/Totp.stories.tsx
diff --git a/src/account/Fallback.tsx b/src/account/Fallback.tsx
index 35a16f49..8ef67146 100644
--- a/src/account/Fallback.tsx
+++ b/src/account/Fallback.tsx
@@ -7,6 +7,7 @@ import { assert, type Equals } from "tsafe/assert";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
+const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
export default function Fallback(props: PageProps) {
const { kcContext, ...rest } = props;
@@ -21,6 +22,8 @@ export default function Fallback(props: PageProps) {
return ;
case "account.ftl":
return ;
+ case "totp.ftl":
+ return ;
}
assert>(false);
})()}
diff --git a/src/account/kcContext/KcContext.ts b/src/account/kcContext/KcContext.ts
index 826a4fa0..9b8a75cd 100644
--- a/src/account/kcContext/KcContext.ts
+++ b/src/account/kcContext/KcContext.ts
@@ -3,7 +3,7 @@ import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { type ThemeType } from "keycloakify/bin/constants";
-export type KcContext = KcContext.Password | KcContext.Account | KcContext.Sessions;
+export type KcContext = KcContext.Password | KcContext.Account | KcContext.Sessions | KcContext.Totp;
export declare namespace KcContext {
export type Common = {
@@ -134,6 +134,49 @@ export declare namespace KcContext {
};
stateChecker: string;
};
+
+ export type Totp = Common & {
+ pageId: "totp.ftl";
+ totp: {
+ totpSecretEncoded: string;
+ qrUrl: string;
+ policy: {
+ algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
+ digits: number;
+ lookAheadWindow: number;
+ } & (
+ | {
+ type: "totp";
+ period: number;
+ }
+ | {
+ type: "hotp";
+ initialCounter: number;
+ }
+ );
+ supportedApplications: string[];
+ totpSecretQrCode: string;
+ manualUrl: string;
+ totpSecret: string;
+ otpCredentials: { id: string; userLabel: string }[];
+ };
+ url: {
+ accountUrl: string;
+ passwordUrl: string;
+ totpUrl: string;
+ socialUrl: string;
+ sessionsUrl: string;
+ applicationsUrl: string;
+ logUrl: string;
+ resourceUrl: string;
+ resourcesCommonPath: string;
+ resourcesPath: string;
+ /** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
+ referrerURI?: string;
+ getLogoutUrl: () => string;
+ };
+ stateChecker: string;
+ };
}
{
diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts
index 9b13b89b..0d85c572 100644
--- a/src/account/kcContext/kcContextMocks.ts
+++ b/src/account/kcContext/kcContextMocks.ts
@@ -199,5 +199,27 @@ export const kcContextMocks: KcContext[] = [
]
},
"stateChecker": ""
+ }),
+ id({
+ ...kcContextCommonMock,
+ "pageId": "totp.ftl",
+ totp: {
+ totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
+ qrUrl: "#",
+ totpSecretQrCode:
+ "iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
+ manualUrl: "#",
+ totpSecret: "G4nsI8lQagRMUchH8jEG",
+ otpCredentials: [],
+ supportedApplications: ["FreeOTP", "Google Authenticator"],
+ policy: {
+ algorithm: "HmacSHA1",
+ digits: 6,
+ lookAheadWindow: 1,
+ type: "totp",
+ period: 30
+ }
+ },
+ "stateChecker": ""
})
];
diff --git a/src/account/pages/Totp.tsx b/src/account/pages/Totp.tsx
new file mode 100644
index 00000000..66c0594a
--- /dev/null
+++ b/src/account/pages/Totp.tsx
@@ -0,0 +1,186 @@
+import { clsx } from "keycloakify/tools/clsx";
+import type { PageProps } from "keycloakify/account/pages/PageProps";
+import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function Totp(props: PageProps, I18n>) {
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+ const { getClassName } = useGetClassName({
+ doUseDefaultCss,
+ classes
+ });
+
+ const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
+
+ const { msg, msgStr } = i18n;
+
+ const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
+ "HmacSHA1": "SHA1",
+ "HmacSHA256": "SHA256",
+ "HmacSHA512": "SHA512"
+ };
+
+ return (
+
+ <>
+
+ -
+
{msg("loginTotpStep1")}
+
+
+ {totp.supportedApplications.map(app => (
+ - {msg(app as MessageKey)}
+ ))}
+
+
+
+ {mode && mode == "manual" ? (
+ <>
+ -
+
{msg("loginTotpManualStep2")}
+
+ {totp.totpSecretEncoded}
+
+
+
+ {msg("loginTotpScanBarcode")}
+
+
+
+ -
+
{msg("loginTotpManualStep3")}
+
+
+ -
+ {msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
+
+ -
+ {msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
+
+ -
+ {msg("loginTotpDigits")}: {totp.policy.digits}
+
+ {totp.policy.type === "totp" ? (
+ -
+ {msg("loginTotpInterval")}: {totp.policy.period}
+
+ ) : (
+ -
+ {msg("loginTotpCounter")}: {totp.policy.initialCounter}
+
+ )}
+
+
+
+ >
+ ) : (
+
+ {msg("loginTotpStep2")}
+
+
+
+
+ {msg("loginTotpUnableToScan")}
+
+
+
+ )}
+
+ {msg("loginTotpStep3")}
+ {msg("loginTotpStep3DeviceName")}
+
+
+
+
+ >
+
+ );
+}
diff --git a/src/bin/keycloakify/generateFtl/pageId.ts b/src/bin/keycloakify/generateFtl/pageId.ts
index 70062ac0..3688198c 100644
--- a/src/bin/keycloakify/generateFtl/pageId.ts
+++ b/src/bin/keycloakify/generateFtl/pageId.ts
@@ -27,7 +27,7 @@ export const loginThemePageIds = [
"saml-post-form.ftl"
] as const;
-export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl"] as const;
+export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];
diff --git a/stories/account/pages/Sessions.stories.tsx b/stories/account/pages/Sessions.stories.tsx
index b5d882aa..250c53b0 100644
--- a/stories/account/pages/Sessions.stories.tsx
+++ b/stories/account/pages/Sessions.stories.tsx
@@ -23,10 +23,4 @@ export default meta;
export const Default = () => ;
-export const WithMessage = () => (
-
-);
+export const WithMessage = () => ;
diff --git a/stories/account/pages/Totp.stories.tsx b/stories/account/pages/Totp.stories.tsx
new file mode 100644
index 00000000..4807f799
--- /dev/null
+++ b/stories/account/pages/Totp.stories.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import type { ComponentMeta } from "@storybook/react";
+import { createPageStory } from "../createPageStory";
+
+const pageId = "totp.ftl";
+
+const { PageStory } = createPageStory({ pageId });
+
+const meta: ComponentMeta = {
+ title: `account/${pageId}`,
+ component: PageStory,
+ parameters: {
+ viewMode: "story",
+ previewTabs: {
+ "storybook/docs/panel": {
+ hidden: true
+ }
+ }
+ }
+};
+
+export default meta;
+
+export const Default = () => (
+
+);
From 33b99172296340939683a57ea8a40c8b5f685350 Mon Sep 17 00:00:00 2001
From: George Litos
Date: Mon, 19 Feb 2024 08:58:27 +0200
Subject: [PATCH 3/7] fix: locales in account totp page
---
src/account/TemplateProps.ts | 8 +-
src/account/kcContext/KcContext.ts | 2 +
src/account/kcContext/kcContextMocks.ts | 4 +-
src/account/lib/useGetClassName.ts | 8 +-
src/account/pages/Totp.tsx | 101 ++++++++++++------------
5 files changed, 68 insertions(+), 55 deletions(-)
diff --git a/src/account/TemplateProps.ts b/src/account/TemplateProps.ts
index 1f20c599..c19f3312 100644
--- a/src/account/TemplateProps.ts
+++ b/src/account/TemplateProps.ts
@@ -18,4 +18,10 @@ export type ClassKey =
| "kcButtonPrimaryClass"
| "kcButtonLargeClass"
| "kcButtonDefaultClass"
- | "kcContentWrapperClass";
+ | "kcContentWrapperClass"
+ | "kcFormClass"
+ | "kcFormGroupClass"
+ | "kcInputWrapperClass"
+ | "kcLabelClass"
+ | "kcInputClass"
+ | "kcInputErrorMessageClass";
diff --git a/src/account/kcContext/KcContext.ts b/src/account/kcContext/KcContext.ts
index 9b8a75cd..ca356144 100644
--- a/src/account/kcContext/KcContext.ts
+++ b/src/account/kcContext/KcContext.ts
@@ -160,6 +160,8 @@ export declare namespace KcContext {
totpSecret: string;
otpCredentials: { id: string; userLabel: string }[];
};
+ mode?: "qr" | "manual" | undefined | null;
+ isAppInitiatedAction: boolean;
url: {
accountUrl: string;
passwordUrl: string;
diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts
index 0d85c572..0c7b7c52 100644
--- a/src/account/kcContext/kcContextMocks.ts
+++ b/src/account/kcContext/kcContextMocks.ts
@@ -211,7 +211,7 @@ export const kcContextMocks: KcContext[] = [
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
- supportedApplications: ["FreeOTP", "Google Authenticator"],
+ supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
policy: {
algorithm: "HmacSHA1",
digits: 6,
@@ -220,6 +220,8 @@ export const kcContextMocks: KcContext[] = [
period: 30
}
},
+ mode: "qr",
+ isAppInitiatedAction: false,
"stateChecker": ""
})
];
diff --git a/src/account/lib/useGetClassName.ts b/src/account/lib/useGetClassName.ts
index f0ee87af..15397ade 100644
--- a/src/account/lib/useGetClassName.ts
+++ b/src/account/lib/useGetClassName.ts
@@ -9,6 +9,12 @@ export const { useGetClassName } = createUseClassName({
"kcContentWrapperClass": "row",
"kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg",
- "kcButtonDefaultClass": "btn-default"
+ "kcButtonDefaultClass": "btn-default",
+ "kcFormClass": "form-horizontal",
+ "kcFormGroupClass": "form-group",
+ "kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
+ "kcLabelClass": "control-label",
+ "kcInputClass": "form-control",
+ "kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
}
});
diff --git a/src/account/pages/Totp.tsx b/src/account/pages/Totp.tsx
index 66c0594a..8ce4eb15 100644
--- a/src/account/pages/Totp.tsx
+++ b/src/account/pages/Totp.tsx
@@ -3,6 +3,7 @@ import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
+import { MessageKey } from "keycloakify/account/i18n/i18n";
export default function Totp(props: PageProps, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@@ -11,7 +12,7 @@ export default function Totp(props: PageProps
<>
+
+
+
{msg("changePasswordHtmlTitle")}
+
+
+ {msg("allFieldsRequired")}
+
+
-
-
{msg("loginTotpStep1")}
+ {msg("totpStep1")}
{totp.supportedApplications.map(app => (
- - {msg(app as MessageKey)}
+ - {msg(app as MessageKey)}
))}
@@ -38,36 +47,36 @@ export default function Totp(props: PageProps
-
-
{msg("loginTotpManualStep2")}
+ {msg("totpManualStep2")}
{totp.totpSecretEncoded}
- {msg("loginTotpScanBarcode")}
+ {msg("totpScanBarcode")}
-
-
{msg("loginTotpManualStep3")}
+ {msg("totpManualStep3")}
-
- {msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
+ {msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
-
- {msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
+ {msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
-
- {msg("loginTotpDigits")}: {totp.policy.digits}
+ {msg("totpDigits")}: {totp.policy.digits}
{totp.policy.type === "totp" ? (
-
- {msg("loginTotpInterval")}: {totp.policy.period}
+ {msg("totpInterval")}: {totp.policy.period}
) : (
-
- {msg("loginTotpCounter")}: {totp.policy.initialCounter}
+ {msg("totpCounter")}: {totp.policy.initialCounter}
)}
@@ -76,31 +85,32 @@ export default function Totp(props: PageProps
) : (
-
-
{msg("loginTotpStep2")}
+ {msg("totpStep2")}
- {msg("loginTotpUnableToScan")}
+ {msg("totpUnableToScan")}
)}
-
-
{msg("loginTotpStep3")}
- {msg("loginTotpStep3DeviceName")}
+ {msg("totpStep3")}
+ {msg("totpStep3DeviceName")}
+ {/*