diff --git a/src/account/Fallback.tsx b/src/account/Fallback.tsx index 8fe62daf..d19f3227 100644 --- a/src/account/Fallback.tsx +++ b/src/account/Fallback.tsx @@ -3,6 +3,7 @@ import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { I18n } from "keycloakify/account/i18n"; import type { KcContext } from "./kcContext"; import { assert, type Equals } from "tsafe/assert"; +import FederatedIdentity from "./pages/FederatedIdentity"; const Password = lazy(() => import("keycloakify/account/pages/Password")); const Account = lazy(() => import("keycloakify/account/pages/Account")); @@ -30,6 +31,8 @@ export default function Fallback(props: PageProps) { return ; case "log.ftl": return ; + case "federatedIdentity.ftl": + return ; } assert>(false); })()} diff --git a/src/account/kcContext/KcContext.ts b/src/account/kcContext/KcContext.ts index f65a0a7f..9b32c0cf 100644 --- a/src/account/kcContext/KcContext.ts +++ b/src/account/kcContext/KcContext.ts @@ -3,7 +3,14 @@ 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 | KcContext.Totp | KcContext.Applications | KcContext.Log; +export type KcContext = + | KcContext.Password + | KcContext.Account + | KcContext.Sessions + | KcContext.Totp + | KcContext.Applications + | KcContext.Log + | KcContext.FederatedIdentity; export declare namespace KcContext { export type Common = { @@ -27,6 +34,7 @@ export declare namespace KcContext { sessionsUrl: string; applicationsUrl: string; logUrl: string; + logoutUrl: string; resourceUrl: string; resourcesCommonPath: string; resourcesPath: string; @@ -91,15 +99,6 @@ export declare namespace KcContext { username?: string; }; properties: Record; - sessions: { - sessions: { - ipAddress: string; - started?: any; - lastAccess?: any; - expires?: any; - clients: string[]; - }[]; - }; }; export type Password = Common & { @@ -126,11 +125,12 @@ export declare namespace KcContext { pageId: "sessions.ftl"; sessions: { sessions: { - ipAddress: string; - started?: any; - lastAccess?: any; - expires?: any; + expires: string; clients: string[]; + ipAddress: string; + started: string; + lastAccess: string; + id: string; }[]; }; stateChecker: string; @@ -193,12 +193,21 @@ export declare namespace KcContext { stateChecker: string; applications: { applications: { - realmRolesAvailable: { name: string; description: string }[]; + realmRolesAvailable: { + name: string; + description: string; + compositesStream?: Record; + clientRole?: boolean; + composite?: boolean; + id?: string; + containerId?: string; + attributes?: Record; + }[]; resourceRolesAvailable: Record< string, { roleName: string; - roleDescription: string; + roleDescription?: string; clientName: string; clientId: string; }[] @@ -207,41 +216,44 @@ export declare namespace KcContext { 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; + attributes: Record; + authenticationFlowBindingOverrides: Record; + baseUrl?: string; + bearerOnly: boolean; + clientAuthenticatorType: string; + clientId: string; consentRequired: boolean; - notBefore: string; - rolesStream: Record; - protocol: string; - dynamicScope: boolean; + consentScreenText: string; + description: string; directAccessGrantsEnabled: boolean; - name: string; - serviceAccountsEnabled: boolean; + displayOnConsentScreen: boolean; + dynamicScope: boolean; + enabled: boolean; frontchannelLogout: boolean; - nodeReRegistrationTimeout: string; + fullScopeAllowed: boolean; + id: string; implicitFlowEnabled: boolean; - baseUrl: string; - webOrigins: string[]; + includeInTokenScope: boolean; + managementUrl: string; + name?: string; + nodeReRegistrationTimeout: string; + notBefore: string; + protocol: string; + protocolMappersStream: Record; + publicClient: boolean; realm: Record; + realmScopeMappingsStream: Record; + redirectUris: string[]; + registeredNodes: Record; + rolesStream: Record; + rootUrl?: string; + scopeMappingsStream: Record; + secret: string; + serviceAccountsEnabled: boolean; + standardFlowEnabled: boolean; + surrogateAuthRequired: boolean; + webOrigins: string[]; }; }[]; }; @@ -254,11 +266,25 @@ export declare namespace KcContext { date: string | number | Date; event: string; ipAddress: string; - client: any; - details: any[]; + client: string; + details: { value: string; key: string }[]; }[]; }; }; + + export type FederatedIdentity = Common & { + pageId: "federatedIdentity.ftl"; + stateChecker: string; + federatedIdentity: { + identities: { + providerId: string; + displayName: string; + userName: string; + connected: boolean; + }[]; + removeLinkPossible: boolean; + }; + }; } { diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts index 383d94c2..1053b367 100644 --- a/src/account/kcContext/kcContextMocks.ts +++ b/src/account/kcContext/kcContextMocks.ts @@ -17,6 +17,7 @@ export const kcContextCommonMock: KcContext.Common = { "resourceUrl": "#", "accountUrl": "#", "applicationsUrl": "#", + "logoutUrl": "#", "getLogoutUrl": () => "#", "logUrl": "#", "passwordUrl": "#", @@ -156,17 +157,6 @@ 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"] - } - ] } }; @@ -200,12 +190,12 @@ export const kcContextMocks: KcContext[] = [ "sessions": { "sessions": [ { - ...kcContextCommonMock.sessions, "ipAddress": "127.0.0.1", "started": new Date().toString(), "lastAccess": new Date().toString(), "expires": new Date().toString(), - "clients": ["Chrome", "Firefox"] + "clients": ["Chrome", "Firefox"], + "id": "f8951177-817d-4a70-9c02-86d3c170fe51" } ] }, @@ -246,9 +236,25 @@ export const kcContextMocks: KcContext[] = [ "event": "login", "ipAddress": "172.17.0.1", "client": "security-admin-console", - "details": ["auth_method = openid-connect, username = admin"] + "details": [{ key: "openid-connect", value: "admin" }] } ] } + }), + id({ + ...kcContextCommonMock, + "stateChecker": "", + "pageId": "federatedIdentity.ftl", + "federatedIdentity": { + "identities": [ + { + "providerId": "keycloak-oidc", + "displayName": "keycloak-oidc", + "userName": "John", + "connected": true + } + ], + "removeLinkPossible": true + } }) ]; diff --git a/src/account/pages/Applications.tsx b/src/account/pages/Applications.tsx index d1b82906..051fb2d1 100644 --- a/src/account/pages/Applications.tsx +++ b/src/account/pages/Applications.tsx @@ -61,10 +61,10 @@ export default function Applications(props: PageProps {!isArrayWithEmptyObject(application.realmRolesAvailable) && - application.realmRolesAvailable.map(role => ( + application.realmRolesAvailable.map((role, index) => ( {role.description ? advancedMsg(role.description) : advancedMsg(role.name)} - {role !== application.realmRolesAvailable[application.realmRolesAvailable.length - 1] && ", "} + {index < application.realmRolesAvailable.length - 1 && ", "} ))} {!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "} diff --git a/src/account/pages/FederatedIdentity.tsx b/src/account/pages/FederatedIdentity.tsx new file mode 100644 index 00000000..91833952 --- /dev/null +++ b/src/account/pages/FederatedIdentity.tsx @@ -0,0 +1,58 @@ +import { PageProps } from "keycloakify/account"; +import { I18n } from "keycloakify/account/i18n"; +import { KcContext } from "keycloakify/account/kcContext"; + +export default function FederatedIdentity(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, classes, Template } = props; + + const { url, federatedIdentity, stateChecker } = kcContext; + const { msg } = i18n; + return ( + + ); +} diff --git a/src/account/pages/Log.tsx b/src/account/pages/Log.tsx index 392873f5..cbec31dd 100644 --- a/src/account/pages/Log.tsx +++ b/src/account/pages/Log.tsx @@ -2,7 +2,7 @@ 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"; +import { useGetClassName } from "keycloakify/account/lib/useGetClassName"; export default function Log(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, classes, Template } = props; diff --git a/src/account/pages/Sessions.tsx b/src/account/pages/Sessions.tsx index 1bb46355..b97338a2 100644 --- a/src/account/pages/Sessions.tsx +++ b/src/account/pages/Sessions.tsx @@ -12,12 +12,9 @@ export default function Sessions(props: PageProps
diff --git a/src/account/pages/Totp.tsx b/src/account/pages/Totp.tsx index 838d0bf7..201d11d6 100644 --- a/src/account/pages/Totp.tsx +++ b/src/account/pages/Totp.tsx @@ -7,6 +7,7 @@ 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 @@ -78,7 +79,7 @@ export default function Totp(props: PageProps{msg("totpStep1")}

    - {totp.supportedApplications.map(app => ( + {totp.supportedApplications?.map(app => (
  • {msg(app as MessageKey)}
  • ))}
@@ -99,28 +100,26 @@ export default function Totp(props: PageProps
  • {msg("totpManualStep3")}

    -

    -

      -
    • - {msg("totpType")}: {msg(`totp.${totp.policy.type}`)} +
        +
      • + {msg("totpType")}: {msg(`totp.${totp.policy.type}`)} +
      • +
      • + {msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} +
      • +
      • + {msg("totpDigits")}: {totp.policy.digits} +
      • + {totp.policy.type === "totp" ? ( +
      • + {msg("totpInterval")}: {totp.policy.period}
      • -
      • - {msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} + ) : ( +
      • + {msg("totpCounter")}: {totp.policy.initialCounter}
      • -
      • - {msg("totpDigits")}: {totp.policy.digits} -
      • - {totp.policy.type === "totp" ? ( -
      • - {msg("totpInterval")}: {totp.policy.period} -
      • - ) : ( -
      • - {msg("totpCounter")}: {totp.policy.initialCounter} -
      • - )} -
      -

      + )} +
  • ) : ( diff --git a/src/bin/keycloakify/generateFtl/pageId.ts b/src/bin/keycloakify/generateFtl/pageId.ts index e0dce92c..cd1083c8 100644 --- a/src/bin/keycloakify/generateFtl/pageId.ts +++ b/src/bin/keycloakify/generateFtl/pageId.ts @@ -27,7 +27,15 @@ export const loginThemePageIds = [ "saml-post-form.ftl" ] as const; -export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const; +export const accountThemePageIds = [ + "password.ftl", + "account.ftl", + "sessions.ftl", + "totp.ftl", + "applications.ftl", + "log.ftl", + "federatedIdentity.ftl" +] as const; export type LoginThemePageId = (typeof loginThemePageIds)[number]; export type AccountThemePageId = (typeof accountThemePageIds)[number]; diff --git a/stories/account/pages/Account.stories.tsx b/stories/account/pages/Account.stories.tsx index 64f1d8ca..5e197369 100644 --- a/stories/account/pages/Account.stories.tsx +++ b/stories/account/pages/Account.stories.tsx @@ -1,24 +1,19 @@ import React from "react"; -import type { ComponentMeta } from "@storybook/react"; +import { Meta, StoryObj } from "@storybook/react"; import { createPageStory } from "../createPageStory"; const pageId = "account.ftl"; const { PageStory } = createPageStory({ pageId }); -const meta: ComponentMeta = { - title: `account/${pageId}`, - component: PageStory, - parameters: { - viewMode: "story", - previewTabs: { - "storybook/docs/panel": { - hidden: true - } - } - } -}; - +const meta = { + title: "account/Account", + component: PageStory +} satisfies Meta; export default meta; -export const Default = () => ; +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; diff --git a/stories/account/pages/Authenticator.stories.tsx b/stories/account/pages/Authenticator.stories.tsx new file mode 100644 index 00000000..2f229056 --- /dev/null +++ b/stories/account/pages/Authenticator.stories.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { createPageStory } from "../createPageStory"; + +const { PageStory } = createPageStory({ + pageId: "totp.ftl" +}); + +const meta = { + title: "account/Authenticator", + component: PageStory +} satisfies Meta; +export default meta; + +export const Default = () => ( + +); + +export const WithTotpEnabled = () => ( + +); + +export const WithManualMode = () => ( + +); + +export const MoreThanOneTotpProviders = () => ( + +); diff --git a/stories/account/pages/FederatedIdentity.stories.tsx b/stories/account/pages/FederatedIdentity.stories.tsx new file mode 100644 index 00000000..ab9d782c --- /dev/null +++ b/stories/account/pages/FederatedIdentity.stories.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { createPageStory } from "../createPageStory"; + +const pageId = "federatedIdentity.ftl"; + +const { PageStory } = createPageStory({ pageId }); + +const meta = { + title: "account/FederatedIdentity", + component: PageStory +} satisfies Meta; + +export default meta; + +export const Default = () => ; + +export const NotConnected = () => ( + +); diff --git a/stories/account/pages/Log.stories.tsx b/stories/account/pages/Log.stories.tsx new file mode 100644 index 00000000..8e1fdfe5 --- /dev/null +++ b/stories/account/pages/Log.stories.tsx @@ -0,0 +1,354 @@ +import React from "react"; +import { Meta } from "@storybook/react"; +import { createPageStory } from "../createPageStory"; + +const pageId = "log.ftl"; + +const { PageStory } = createPageStory({ + pageId +}); + +const meta = { + title: "account/Log", + component: PageStory +} satisfies Meta; + +export default meta; + +export const Default = () => ( + +); diff --git a/stories/account/pages/Pasword.stories.tsx b/stories/account/pages/Pasword.stories.tsx index 637a0c03..e385fcfa 100644 --- a/stories/account/pages/Pasword.stories.tsx +++ b/stories/account/pages/Pasword.stories.tsx @@ -1,23 +1,15 @@ import React from "react"; -import type { ComponentMeta } from "@storybook/react"; +import { Meta } from "@storybook/react"; import { createPageStory } from "../createPageStory"; const pageId = "password.ftl"; const { PageStory } = createPageStory({ pageId }); -const meta: ComponentMeta = { - title: `account/${pageId}`, - component: PageStory, - parameters: { - viewMode: "story", - previewTabs: { - "storybook/docs/panel": { - hidden: true - } - } - } -}; +const meta = { + title: "account/Password", + component: PageStory +} satisfies Meta; export default meta; diff --git a/stories/account/pages/Sessions.stories.tsx b/stories/account/pages/Sessions.stories.tsx index 250c53b0..462a2c98 100644 --- a/stories/account/pages/Sessions.stories.tsx +++ b/stories/account/pages/Sessions.stories.tsx @@ -1,26 +1,55 @@ import React from "react"; -import type { ComponentMeta } from "@storybook/react"; +import { Meta } 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 - } - } - } -}; +const meta = { + title: "account/Sessions", + component: PageStory +} satisfies Meta; export default meta; -export const Default = () => ; +export const Default = () => ( + +); -export const WithMessage = () => ; +export const WithError = () => ( + +); diff --git a/stories/account/pages/Totp.stories.tsx b/stories/account/pages/Totp.stories.tsx index 265d76a6..c6e9f4ad 100644 --- a/stories/account/pages/Totp.stories.tsx +++ b/stories/account/pages/Totp.stories.tsx @@ -1,23 +1,15 @@ import React from "react"; -import type { ComponentMeta } from "@storybook/react"; +import { Meta } 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 - } - } - } -}; +const meta = { + title: "account/Authenticator", + component: PageStory +} satisfies Meta; export default meta;