diff --git a/keycloakify/dist_keycloak/keycloak-theme-for-kc-22-to-25.jar b/keycloakify/dist_keycloak/keycloak-theme-for-kc-22-to-25.jar index 6eb7459d..b56f97f6 100644 Binary files a/keycloakify/dist_keycloak/keycloak-theme-for-kc-22-to-25.jar and b/keycloakify/dist_keycloak/keycloak-theme-for-kc-22-to-25.jar differ diff --git a/keycloakify/dist_keycloak/keycloak-theme-for-kc-all-other-versions.jar b/keycloakify/dist_keycloak/keycloak-theme-for-kc-all-other-versions.jar index 6dafb83b..984669f8 100644 Binary files a/keycloakify/dist_keycloak/keycloak-theme-for-kc-all-other-versions.jar and b/keycloakify/dist_keycloak/keycloak-theme-for-kc-all-other-versions.jar differ diff --git a/keycloakify/package.json b/keycloakify/package.json index 79c88538..0c728d1b 100755 --- a/keycloakify/package.json +++ b/keycloakify/package.json @@ -12,7 +12,10 @@ "build": "tsc && vite build", "build-keycloak-theme": "npm run build && keycloakify build", "storybook": "storybook dev -p 6006", - "format": "prettier . --write" + "format": "prettier . --write", + "start-keycloak": "npx keycloakify start-keycloak", + "eject-page": "npx keycloakify eject-page", + "add-story": "npx keycloakify add-story" }, "license": "MIT", "keywords": [], diff --git a/keycloakify/src/components/ui/alert.tsx b/keycloakify/src/components/ui/alert.tsx index 40a311c0..ff794542 100644 --- a/keycloakify/src/components/ui/alert.tsx +++ b/keycloakify/src/components/ui/alert.tsx @@ -1,61 +1,63 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( - "flex items-center gap-2 w-full rounded-lg border px-4 py-3 text-sm [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - error: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - success: "border-lime-500/50 text-lime-500 dark:border-lime-500 [&>svg]:text-lime-500", - warning: "border-yellow-500/50 text-yellow-500 dark:border-yellow-500 [&>svg]:text-yellow-500", - info: "border-blue-500/50 text-blue-500 dark:border-blue-500 [&>svg]:text-blue-500", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "flex items-center gap-2 w-full rounded-lg border px-4 py-3 text-sm [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + error: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + success: + "border-lime-500/50 text-lime-500 dark:border-lime-500 [&>svg]:text-lime-500", + warning: + "border-yellow-500/50 text-yellow-500 dark:border-yellow-500 [&>svg]:text-yellow-500", + info: "border-blue-500/50 text-blue-500 dark:border-blue-500 [&>svg]:text-blue-500" + } + }, + defaultVariants: { + variant: "default" + } + } +); const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps + HTMLDivElement, + React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" +
+)); +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" +
+)); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" +
+)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/keycloakify/src/login/KcPage.tsx b/keycloakify/src/login/KcPage.tsx index 2c15483c..557f7369 100644 --- a/keycloakify/src/login/KcPage.tsx +++ b/keycloakify/src/login/KcPage.tsx @@ -12,6 +12,10 @@ const UserProfileFormFields = lazy( () => import("keycloakify/login/UserProfileFormFields") ); const Login = lazy(() => import("./pages/Login")); +const Info = lazy(() => import("./pages/Info")); +const Error = lazy(() => import("./pages/Error")); +const LoginUpdateProfile = lazy(() => import("./pages/LoginUpdateProfile")); +const LoginPageExpired = lazy(() => import("./pages/LoginPageExpired")); const doMakeUserConfirmPassword = true; @@ -40,6 +44,40 @@ function KcPageContextualized(props: { kcContext: KcContext }) { doUseDefaultCss={false} /> ); + case "info.ftl": + return ( + + ); + case "error.ftl": + return ( + + ); + case "login-update-profile.ftl": + return ( + + ); + case "login-page-expired.ftl": + return ( + + ); default: return ( ) {
{(() => { const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( -

{headerNode}

+

+ {headerNode} +

) : (
diff --git a/keycloakify/src/login/pages/Error.stories.tsx b/keycloakify/src/login/pages/Error.stories.tsx new file mode 100644 index 00000000..9c1e5a7b --- /dev/null +++ b/keycloakify/src/login/pages/Error.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { createKcPageStory } from "../KcPageStory"; + +const { KcPageStory } = createKcPageStory({ pageId: "error.ftl" }); + +const meta = { + title: "login/error.ftl", + component: KcPageStory +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; + +export const WithAnotherMessage: Story = { + render: () => ( + + ) +}; + +export const WithHtmlErrorMessage: Story = { + render: () => ( + Error: Something went wrong. Go back" + } + }} + /> + ) +}; +export const FrenchError: Story = { + render: () => ( + + ) +}; +export const WithSkipLink: Story = { + render: () => ( + + ) +}; diff --git a/keycloakify/src/login/pages/Error.tsx b/keycloakify/src/login/pages/Error.tsx new file mode 100644 index 00000000..a7d3d689 --- /dev/null +++ b/keycloakify/src/login/pages/Error.tsx @@ -0,0 +1,34 @@ +import type { PageProps } from "keycloakify/login/pages/PageProps"; +import { kcSanitize } from "keycloakify/lib/kcSanitize"; +import type { KcContext } from "../KcContext"; +import type { I18n } from "../i18n"; + +export default function Error(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + + const { message, client, skipLink } = kcContext; + + const { msg } = i18n; + + return ( + + ); +} diff --git a/keycloakify/src/login/pages/Info.stories.tsx b/keycloakify/src/login/pages/Info.stories.tsx new file mode 100644 index 00000000..33123f9f --- /dev/null +++ b/keycloakify/src/login/pages/Info.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { createKcPageStory } from "../KcPageStory"; + +const { KcPageStory } = createKcPageStory({ pageId: "info.ftl" }); + +const meta = { + title: "login/info.ftl", + component: KcPageStory +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + ) +}; + +export const WithLinkBack: Story = { + render: () => ( + + ) +}; + +export const WithRequiredActions: Story = { + render: () => ( + + ) +}; +export const WithPageRedirect: Story = { + render: () => ( + + ) +}; +export const WithoutClientBaseUrl: Story = { + render: () => ( + + ) +}; +export const WithMessageHeader: Story = { + render: () => ( + + ) +}; +export const WithAdvancedMessage: Story = { + render: () => ( + important information." } + }} + /> + ) +}; diff --git a/keycloakify/src/login/pages/Info.tsx b/keycloakify/src/login/pages/Info.tsx new file mode 100644 index 00000000..cd2e4405 --- /dev/null +++ b/keycloakify/src/login/pages/Info.tsx @@ -0,0 +1,80 @@ +import type { PageProps } from "keycloakify/login/pages/PageProps"; +import { kcSanitize } from "keycloakify/lib/kcSanitize"; +import type { KcContext } from "../KcContext"; +import type { I18n } from "../i18n"; + +export default function Info(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + + const { advancedMsgStr, msg } = i18n; + + const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext; + + return ( + + ); +} diff --git a/keycloakify/src/login/pages/LoginPageExpired.stories.tsx b/keycloakify/src/login/pages/LoginPageExpired.stories.tsx new file mode 100644 index 00000000..46e9f580 --- /dev/null +++ b/keycloakify/src/login/pages/LoginPageExpired.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { createKcPageStory } from "../KcPageStory"; + +const { KcPageStory } = createKcPageStory({ pageId: "login-page-expired.ftl" }); + +const meta = { + title: "login/login-page-expired.ftl", + component: KcPageStory +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; + +/** + * WithErrorMessage: + * - Purpose: Tests behavior when an error message is displayed along with the page expiration message. + * - Scenario: Simulates a case where the session expired due to an error, and an error message is displayed alongside the expiration message. + * - Key Aspect: Ensures that error messages are displayed correctly in addition to the page expiration notice. + */ +export const WithErrorMessage: Story = { + render: () => ( + + ) +}; diff --git a/keycloakify/src/login/pages/LoginPageExpired.tsx b/keycloakify/src/login/pages/LoginPageExpired.tsx new file mode 100644 index 00000000..741c55a1 --- /dev/null +++ b/keycloakify/src/login/pages/LoginPageExpired.tsx @@ -0,0 +1,28 @@ +import type { PageProps } from "keycloakify/login/pages/PageProps"; +import type { KcContext } from "../KcContext"; +import type { I18n } from "../i18n"; + +export default function LoginPageExpired(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + + const { url } = kcContext; + + const { msg } = i18n; + + return ( + + ); +} diff --git a/keycloakify/src/login/pages/LoginUpdateProfile.stories.tsx b/keycloakify/src/login/pages/LoginUpdateProfile.stories.tsx new file mode 100644 index 00000000..8e9ff045 --- /dev/null +++ b/keycloakify/src/login/pages/LoginUpdateProfile.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { createKcPageStory } from "../KcPageStory"; + +const { KcPageStory } = createKcPageStory({ pageId: "login-update-profile.ftl" }); + +const meta = { + title: "login/login-update-profile.ftl", + component: KcPageStory +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; + +/** + * WithProfileError: + * - Purpose: Tests when an error occurs in one or more profile fields (e.g., invalid email format). + * - Scenario: The component displays error messages next to the affected fields. + * - Key Aspect: Ensures the profile fields show error messages when validation fails. + */ +export const WithProfileError: Story = { + render: () => ( + field === "email", + get: () => "Invalid email format" + }, + isAppInitiatedAction: false + }} + /> + ) +}; diff --git a/keycloakify/src/login/pages/LoginUpdateProfile.tsx b/keycloakify/src/login/pages/LoginUpdateProfile.tsx new file mode 100644 index 00000000..a7477961 --- /dev/null +++ b/keycloakify/src/login/pages/LoginUpdateProfile.tsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; +import { getKcClsx } from "keycloakify/login/lib/kcClsx"; +import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps"; +import type { PageProps } from "keycloakify/login/pages/PageProps"; +import type { KcContext } from "../KcContext"; +import type { I18n } from "../i18n"; + +type LoginUpdateProfileProps = PageProps, I18n> & { + UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; + doMakeUserConfirmPassword: boolean; +}; + +export default function LoginUpdateProfile(props: LoginUpdateProfileProps) { + const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props; + + const { kcClsx } = getKcClsx({ + doUseDefaultCss, + classes + }); + + const { messagesPerField, url, isAppInitiatedAction } = kcContext; + + const { msg, msgStr } = i18n; + + const [isFormSubmittable, setIsFormSubmittable] = useState(false); + + return ( + + ); +} diff --git a/keycloakify/src/main.tsx b/keycloakify/src/main.tsx index 94add8f0..f570dcee 100644 --- a/keycloakify/src/main.tsx +++ b/keycloakify/src/main.tsx @@ -5,37 +5,37 @@ import { KcPage } from "./kc.gen"; // The following block can be uncommented to test a specific page with `yarn dev` // Don't forget to comment back or your bundle size will increase -import { getKcContextMock } from "./login/KcPageStory"; +// import { getKcContextMock } from "./login/KcPageStory"; -if (import.meta.env.DEV) { - window.kcContext = getKcContextMock({ - pageId: "login.ftl", - overrides: { - message: { - type: "error", - summary: "This is an error message", - }, - social: { - providers: [ - { - loginUrl: "http://localhost:8080/auth/realms/hephaestus/broker/github/endpoint", - alias: "github", - providerId: "github", - displayName: "GitHub", - iconClasses: "fa fa-github", - }, - { - loginUrl: "http://localhost:8080/auth/realms/hephaestus/broker/gitlab-lrz/endpoint", - alias: "gitlab", - providerId: "gitlab-lrz", - displayName: "GitLab LRZ", - iconClasses: "", // None since it is OpenID Connect - } - ], - }, - } - }); -} +// if (import.meta.env.DEV) { +// window.kcContext = getKcContextMock({ +// pageId: "login.ftl", +// overrides: { +// message: { +// type: "error", +// summary: "This is an error message", +// }, +// social: { +// providers: [ +// { +// loginUrl: "http://localhost:8080/auth/realms/hephaestus/broker/github/endpoint", +// alias: "github", +// providerId: "github", +// displayName: "GitHub", +// iconClasses: "fa fa-github", +// }, +// { +// loginUrl: "http://localhost:8080/auth/realms/hephaestus/broker/gitlab-lrz/endpoint", +// alias: "gitlab", +// providerId: "gitlab-lrz", +// displayName: "GitLab LRZ", +// iconClasses: "", // None since it is OpenID Connect +// } +// ], +// }, +// } +// }); +// } createRoot(document.getElementById("root")!).render( diff --git a/keycloakify/vite.config.ts b/keycloakify/vite.config.ts index 08716087..0b69b916 100644 --- a/keycloakify/vite.config.ts +++ b/keycloakify/vite.config.ts @@ -11,8 +11,10 @@ export default defineConfig({ keycloakify({ themeName: "hephaestus", accountThemeImplementation: "none", - postBuild: async (buildContext) => { - await fs.rm(path.join(buildContext.keycloakifyBuildDirPath, ".gitignore")); + postBuild: async buildContext => { + await fs.rm( + path.join(buildContext.keycloakifyBuildDirPath, ".gitignore") + ); } }) ],