diff --git a/src/account/Fallback.tsx b/src/account/Fallback.tsx index 50e9225a..8fe62daf 100644 --- a/src/account/Fallback.tsx +++ b/src/account/Fallback.tsx @@ -6,6 +6,10 @@ 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")); +const Applications = lazy(() => import("keycloakify/account/pages/Applications")); +const Log = lazy(() => import("keycloakify/account/pages/Log")); export default function Fallback(props: PageProps) { const { kcContext, ...rest } = props; @@ -16,8 +20,16 @@ export default function Fallback(props: PageProps) { switch (kcContext.pageId) { case "password.ftl": return ; + case "sessions.ftl": + return ; case "account.ftl": return ; + case "totp.ftl": + return ; + case "applications.ftl": + return ; + case "log.ftl": + return ; } assert>(false); })()} diff --git a/src/account/TemplateProps.ts b/src/account/TemplateProps.ts index 10ebf9aa..c19f3312 100644 --- a/src/account/TemplateProps.ts +++ b/src/account/TemplateProps.ts @@ -11,4 +11,17 @@ export type TemplateProps; + sessions: { + sessions: { + ipAddress: string; + started?: any; + lastAccess?: any; + expires?: any; + clients: string[]; + }[]; + }; }; export type Password = Common & { @@ -112,6 +121,144 @@ export declare namespace KcContext { }; stateChecker: string; }; + + export type Sessions = Common & { + pageId: "sessions.ftl"; + sessions: { + sessions: { + ipAddress: string; + started?: any; + lastAccess?: any; + expires?: any; + clients: string[]; + }[]; + }; + stateChecker: string; + }; + + export type Totp = Common & { + pageId: "totp.ftl"; + totp: { + enabled: boolean; + 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 }[]; + }; + mode?: "qr" | "manual" | undefined | null; + isAppInitiatedAction: boolean; + 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; + }; + + export type Applications = Common & { + pageId: "applications.ftl"; + features: { + log: boolean; + identityFederation: boolean; + authorization: boolean; + passwordUpdateSupported: boolean; + }; + stateChecker: string; + applications: { + applications: { + realmRolesAvailable: { name: string; description: string }[]; + resourceRolesAvailable: Record< + string, + { + roleName: string; + roleDescription: string; + clientName: string; + clientId: string; + }[] + >; + additionalGrants: string[]; + clientScopesGranted: string[]; + effectiveUrl?: string; + client: { + consentScreenText: string; + surrogateAuthRequired: boolean; + bearerOnly: boolean; + id: string; + protocolMappersStream: Record; + includeInTokenScope: boolean; + redirectUris: string[]; + fullScopeAllowed: boolean; + registeredNodes: Record; + enabled: boolean; + clientAuthenticatorType: string; + realmScopeMappingsStream: Record; + scopeMappingsStream: Record; + displayOnConsentScreen: boolean; + clientId: string; + rootUrl: string; + authenticationFlowBindingOverrides: Record; + standardFlowEnabled: boolean; + attributes: Record; + publicClient: boolean; + alwaysDisplayInConsole: boolean; + consentRequired: boolean; + notBefore: string; + rolesStream: Record; + protocol: string; + dynamicScope: boolean; + directAccessGrantsEnabled: boolean; + name: string; + serviceAccountsEnabled: boolean; + frontchannelLogout: boolean; + nodeReRegistrationTimeout: string; + implicitFlowEnabled: boolean; + baseUrl: string; + webOrigins: string[]; + realm: Record; + }; + }[]; + }; + }; + + export type Log = Common & { + pageId: "log.ftl"; + log: { + events: { + date: string | number | Date; + event: string; + ipAddress: string; + client: any; + details: any[]; + }[]; + }; + }; } { diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts index 18d41c7f..383d94c2 100644 --- a/src/account/kcContext/kcContextMocks.ts +++ b/src/account/kcContext/kcContextMocks.ts @@ -156,6 +156,17 @@ export const kcContextCommonMock: KcContext.Common = { "css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css", "kcButtonClass": "btn", "kcButtonDefaultClass": "btn-default" + }, + "sessions": { + "sessions": [ + { + "ipAddress": "127.0.0.1", + "started": new Date().toString(), + "lastAccess": new Date().toString(), + "expires": new Date().toString(), + "clients": ["Chrome", "Firefox"] + } + ] } }; @@ -182,5 +193,62 @@ export const kcContextMocks: KcContext[] = [ "editUsernameAllowed": true }, "stateChecker": "" + }), + id({ + ...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": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g" + }), + id({ + ...kcContextCommonMock, + "pageId": "totp.ftl", + "totp": { + "enabled": true, + "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": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"], + "policy": { + "algorithm": "HmacSHA1", + "digits": 6, + "lookAheadWindow": 1, + "type": "totp", + "period": 30 + } + }, + "mode": "qr", + "isAppInitiatedAction": false, + "stateChecker": "" + }), + id({ + ...kcContextCommonMock, + "pageId": "log.ftl", + "log": { + "events": [ + { + "date": "2/21/2024, 1:28:39 PM", + "event": "login", + "ipAddress": "172.17.0.1", + "client": "security-admin-console", + "details": ["auth_method = openid-connect, username = admin"] + } + ] + } }) ]; diff --git a/src/account/lib/useGetClassName.ts b/src/account/lib/useGetClassName.ts index 51366ff0..15397ade 100644 --- a/src/account/lib/useGetClassName.ts +++ b/src/account/lib/useGetClassName.ts @@ -6,8 +6,15 @@ export const { useGetClassName } = createUseClassName({ "kcHtmlClass": undefined, "kcBodyClass": undefined, "kcButtonClass": "btn", + "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/Applications.tsx b/src/account/pages/Applications.tsx new file mode 100644 index 00000000..d1b82906 --- /dev/null +++ b/src/account/pages/Applications.tsx @@ -0,0 +1,138 @@ +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"; + +function isArrayWithEmptyObject(variable: any): boolean { + return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0; +} + +export default function Applications(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, classes, Template } = props; + + const { getClassName } = useGetClassName({ + doUseDefaultCss, + classes + }); + + const { + url, + applications: { applications }, + stateChecker + } = kcContext; + + const { msg, advancedMsg } = i18n; + + return ( + + ); +} diff --git a/src/account/pages/Log.tsx b/src/account/pages/Log.tsx new file mode 100644 index 00000000..392873f5 --- /dev/null +++ b/src/account/pages/Log.tsx @@ -0,0 +1,70 @@ +import type { PageProps } from "keycloakify/account/pages/PageProps"; +import type { KcContext } from "../kcContext"; +import type { I18n } from "../i18n"; +import { Key } from "react"; +import { useGetClassName } from "../lib/useGetClassName"; + +export default function Log(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, classes, Template } = props; + + const { getClassName } = useGetClassName({ + doUseDefaultCss, + classes + }); + + const { log } = kcContext; + + const { msg } = i18n; + + return ( + + ); +} 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 ( + + ); +} diff --git a/src/account/pages/Totp.tsx b/src/account/pages/Totp.tsx new file mode 100644 index 00000000..838d0bf7 --- /dev/null +++ b/src/account/pages/Totp.tsx @@ -0,0 +1,236 @@ +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"; +import { MessageKey } from "keycloakify/account/i18n/i18n"; + +export default function Totp(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + const { getClassName } = useGetClassName({ + doUseDefaultCss, + classes + }); + + const { totp, mode, url, messagesPerField, stateChecker } = kcContext; + + const { msg, msgStr } = i18n; + + const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = { + "HmacSHA1": "SHA1", + "HmacSHA256": "SHA256", + "HmacSHA512": "SHA512" + }; + + return ( + + ); +} diff --git a/src/bin/keycloakify/generateFtl/pageId.ts b/src/bin/keycloakify/generateFtl/pageId.ts index da3525f6..e0dce92c 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", "totp.ftl", "applications.ftl", "log.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..250c53b0 --- /dev/null +++ b/stories/account/pages/Sessions.stories.tsx @@ -0,0 +1,26 @@ +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 = () => ; diff --git a/stories/account/pages/Totp.stories.tsx b/stories/account/pages/Totp.stories.tsx new file mode 100644 index 00000000..265d76a6 --- /dev/null +++ b/stories/account/pages/Totp.stories.tsx @@ -0,0 +1,113 @@ +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 = () => ( + +); + +export const WithTotpEnabled = () => ( + +); + +export const WithManualMode = () => ( + +);