Merge pull request #2 from lordvlad/lordvlad/issue125
feat(devx): introduce storybook
This commit is contained in:
commit
8c61339a7c
18
.github/workflows/ci.yaml
vendored
18
.github/workflows/ci.yaml
vendored
@ -34,6 +34,20 @@ jobs:
|
|||||||
name: build
|
name: build
|
||||||
path: build
|
path: build
|
||||||
|
|
||||||
|
deploy_storybook:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2.1.3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
|
- run: yarn build-storybook -o ./build_storybook
|
||||||
|
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook --dest storybook --add -u "github-actions-bot <actions@github.com>"
|
||||||
|
|
||||||
check_if_version_upgraded:
|
check_if_version_upgraded:
|
||||||
name: Check if version upgrade
|
name: Check if version upgrade
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -53,7 +67,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- check_if_version_upgraded
|
- check_if_version_upgraded
|
||||||
- build
|
- build
|
||||||
# We publish the the docker image only if it's a push on the default branch or if it's a PR from a
|
# We publish the docker image only if it's a push on the default branch or if it's a PR from a
|
||||||
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
|
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
|
||||||
# defined but GitHub Action don't allow it.
|
# defined but GitHub Action don't allow it.
|
||||||
if: |
|
if: |
|
||||||
@ -80,7 +94,7 @@ jobs:
|
|||||||
- check_if_version_upgraded
|
- check_if_version_upgraded
|
||||||
- build
|
- build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# We publish the the docker image only if it's a push on the default branch or if it's a PR from a
|
# We publish the docker image only if it's a push on the default branch or if it's a PR from a
|
||||||
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
|
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
|
||||||
# defined but GitHub Action don't allow it.
|
# defined but GitHub Action don't allow it.
|
||||||
if: |
|
if: |
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -44,3 +44,4 @@ jspm_packages
|
|||||||
|
|
||||||
/build_keycloak
|
/build_keycloak
|
||||||
/build
|
/build
|
||||||
|
/storybook-static
|
16
.storybook/main.js
Normal file
16
.storybook/main.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
"stories": [
|
||||||
|
"../src/keycloak-theme/**/*.stories.tsx",
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
|
"@storybook/preset-create-react-app"
|
||||||
|
],
|
||||||
|
"framework": "@storybook/react",
|
||||||
|
"core": {
|
||||||
|
"builder": "@storybook/builder-webpack5"
|
||||||
|
},
|
||||||
|
"staticDirs": ['../public']
|
||||||
|
}
|
13
.storybook/preview.js
Normal file
13
.storybook/preview.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const parameters = {
|
||||||
|
actions: {argTypesRegex: "^on[A-Z].*"},
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
storySort: (a, b) =>
|
||||||
|
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, {numeric: true}),
|
||||||
|
},
|
||||||
|
}
|
41
.storybook/util.tsx
Normal file
41
.storybook/util.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type {KcContextExtension} from "keycloak-theme/kcContext";
|
||||||
|
import KcApp from "../src/keycloak-theme/KcApp";
|
||||||
|
import {KcContextBase} from "keycloakify";
|
||||||
|
import {getKcContext} from "keycloakify/lib/getKcContext";
|
||||||
|
import {ExtendsKcContextBase} from "keycloakify/src/lib/getKcContext/getKcContextFromWindow";
|
||||||
|
import {DeepPartial} from "keycloakify/src/lib/tools/DeepPartial";
|
||||||
|
|
||||||
|
|
||||||
|
export const socialProviders = [
|
||||||
|
{loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google'},
|
||||||
|
{loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft'},
|
||||||
|
{loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook'},
|
||||||
|
{loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram'},
|
||||||
|
{loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter'},
|
||||||
|
{loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn'},
|
||||||
|
{loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow'},
|
||||||
|
{loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github'},
|
||||||
|
{loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab'},
|
||||||
|
{loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket'},
|
||||||
|
{loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal'},
|
||||||
|
{loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift'},
|
||||||
|
]
|
||||||
|
|
||||||
|
type PageId = (KcContextExtension | KcContextBase)['pageId']
|
||||||
|
export const template = (pageId: PageId) => {
|
||||||
|
type MockData = DeepPartial<ExtendsKcContextBase<KcContextExtension>>;
|
||||||
|
|
||||||
|
const Template = (mockData: MockData) => {
|
||||||
|
const finalMockData = {
|
||||||
|
message: undefined,
|
||||||
|
pageId,
|
||||||
|
...mockData
|
||||||
|
} as MockData
|
||||||
|
if (!("message" in mockData)) mockData["message"] = undefined
|
||||||
|
const {kcContext} = getKcContext<KcContextExtension>({mockPageId: pageId, mockData: [finalMockData]})
|
||||||
|
return <KcApp kcContext={kcContext as NonNullable<typeof kcContext>}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (args: MockData) => Object.assign(Template.bind({}), {args})
|
||||||
|
}
|
||||||
|
|
26
package.json
26
package.json
@ -11,7 +11,9 @@
|
|||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"build-keycloak-theme": "yarn build && keycloakify",
|
"build-keycloak-theme": "yarn build && keycloakify",
|
||||||
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme"
|
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook"
|
||||||
},
|
},
|
||||||
"keycloakify": {
|
"keycloakify": {
|
||||||
"extraPages": [
|
"extraPages": [
|
||||||
@ -26,13 +28,23 @@
|
|||||||
"evt": "^2.4.15",
|
"evt": "^2.4.15",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"keycloak-js": "^21.0.1",
|
"keycloak-js": "^21.0.1",
|
||||||
"keycloakify": "^6.12.7",
|
"keycloakify": "^6.13.1",
|
||||||
"powerhooks": "^0.26.2",
|
"powerhooks": "^0.26.2",
|
||||||
"react": "18.1.0",
|
"react": "18.1.0",
|
||||||
"react-dom": "18.1.0",
|
"react-dom": "18.1.0",
|
||||||
"tsafe": "^1.4.3"
|
"tsafe": "^1.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@storybook/addon-actions": "^6.5.16",
|
||||||
|
"@storybook/addon-essentials": "^6.5.16",
|
||||||
|
"@storybook/addon-interactions": "^6.5.16",
|
||||||
|
"@storybook/addon-links": "^6.5.16",
|
||||||
|
"@storybook/builder-webpack5": "^6.5.16",
|
||||||
|
"@storybook/manager-webpack5": "^6.5.16",
|
||||||
|
"@storybook/node-logger": "^6.5.16",
|
||||||
|
"@storybook/preset-create-react-app": "^4.1.2",
|
||||||
|
"@storybook/react": "^6.5.16",
|
||||||
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@types/node": "^15.3.1",
|
"@types/node": "^15.3.1",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"@types/react-dom": "18.0.4",
|
"@types/react-dom": "18.0.4",
|
||||||
@ -48,7 +60,17 @@
|
|||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
"@typescript-eslint/no-redeclare": "off",
|
"@typescript-eslint/no-redeclare": "off",
|
||||||
"no-labels": "off"
|
"no-labels": "off"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.stories.*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/no-anonymous-default-export": "off"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
32
src/keycloak-theme/Template.stories.tsx
Normal file
32
src/keycloak-theme/Template.stories.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type {ComponentMeta} from '@storybook/react';
|
||||||
|
import KcApp from './KcApp';
|
||||||
|
import {template} from '../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('my-extra-page-1.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Theme/Template',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
export const InFrench = bind({locale: {currentLanguageTag: 'fr'}})
|
||||||
|
export const RealmDisplayNameIsHtml = bind({
|
||||||
|
realm: {
|
||||||
|
displayNameHtml: '<marquee>my realm</marquee>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const NoInternationalization = bind({
|
||||||
|
realm: {
|
||||||
|
internationalizationEnabled: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithGlobalError = bind({
|
||||||
|
message: {type: "error", summary: "This is an error"}
|
||||||
|
})
|
@ -1,11 +1,6 @@
|
|||||||
import { getKcContext } from "keycloakify/lib/getKcContext";
|
import { getKcContext } from "keycloakify/lib/getKcContext";
|
||||||
|
|
||||||
//NOTE: In most of the cases you do not need to overload the KcContext, you can
|
export type KcContextExtension =
|
||||||
// just call getKcContext(...) without type arguments.
|
|
||||||
// You want to overload the KcContext only if:
|
|
||||||
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
|
|
||||||
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
|
|
||||||
export const { kcContext } = getKcContext<
|
|
||||||
// NOTE: A 'keycloakify' field must be added
|
// NOTE: A 'keycloakify' field must be added
|
||||||
// in the package.json to generate theses extra pages
|
// in the package.json to generate theses extra pages
|
||||||
// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
|
// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
|
||||||
@ -14,8 +9,14 @@ export const { kcContext } = getKcContext<
|
|||||||
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
|
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
|
||||||
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
|
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
|
||||||
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
||||||
| { pageId: "register.ftl"; authorizedMailDomains: string[]; }
|
| { pageId: "register.ftl"; authorizedMailDomains: string[]; };
|
||||||
>({
|
|
||||||
|
//NOTE: In most of the cases you do not need to overload the KcContext, you can
|
||||||
|
// just call getKcContext(...) without type arguments.
|
||||||
|
// You want to overload the KcContext only if:
|
||||||
|
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
|
||||||
|
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
|
||||||
|
export const { kcContext } = getKcContext<KcContextExtension>({
|
||||||
// Uncomment to test the login page for development.
|
// Uncomment to test the login page for development.
|
||||||
//mockPageId: "login.ftl",
|
//mockPageId: "login.ftl",
|
||||||
mockData: [
|
mockData: [
|
||||||
|
16
src/keycloak-theme/pages/Error.stories.tsx
Normal file
16
src/keycloak-theme/pages/Error.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {ComponentMeta} from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import {template} from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('error.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Notification/Error',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({message: {type: 'error', summary: 'Something went wrong'}})
|
34
src/keycloak-theme/pages/Error.tsx
Normal file
34
src/keycloak-theme/pages/Error.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// copied and adapted from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Error.tsx
|
||||||
|
import React from "react";
|
||||||
|
import type { PageProps } from "keycloakify"
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
|
||||||
|
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { message, client } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("errorTitle")}
|
||||||
|
formNode={
|
||||||
|
<div id="kc-error-message">
|
||||||
|
<p className="instruction">{message.summary}</p>
|
||||||
|
{client !== undefined && client.baseUrl !== undefined && (
|
||||||
|
<p>
|
||||||
|
<a id="backToApplication" href={client.baseUrl}>
|
||||||
|
{msg("backToApplication")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
Normal file
17
src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('idp-review-user-profile.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/IDP/Review User Profile',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
47
src/keycloak-theme/pages/IdpReviewUserProfile.tsx
Normal file
47
src/keycloak-theme/pages/IdpReviewUserProfile.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function IdpReviewUserProfile(props: PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url } = kcContext;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-idp-review-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
38
src/keycloak-theme/pages/Info.stories.tsx
Normal file
38
src/keycloak-theme/pages/Info.stories.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('info.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Notification/Info',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
|
||||||
|
export const Default = bind({
|
||||||
|
messageHeader: 'Yo, get this:',
|
||||||
|
message: {
|
||||||
|
summary: 'You look good today'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithLinkBack = bind({
|
||||||
|
messageHeader: 'Yo, get this:',
|
||||||
|
message: {
|
||||||
|
summary: 'You look good today'
|
||||||
|
},
|
||||||
|
actionUri: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithRequiredActions = bind({
|
||||||
|
messageHeader: 'Yo, get this:',
|
||||||
|
message: {
|
||||||
|
summary: 'Before you can carry on, you need to do this: '
|
||||||
|
},
|
||||||
|
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL"]
|
||||||
|
})
|
49
src/keycloak-theme/pages/Info.tsx
Normal file
49
src/keycloak-theme/pages/Info.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {assert} from "keycloakify/lib/tools/assert";
|
||||||
|
import type {PageProps} from "keycloakify";
|
||||||
|
import type {KcContext} from "../kcContext";
|
||||||
|
import type {I18n} from "../i18n";
|
||||||
|
|
||||||
|
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
|
||||||
|
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||||
|
|
||||||
|
const {msgStr, msg} = i18n;
|
||||||
|
|
||||||
|
assert(kcContext.message !== undefined);
|
||||||
|
|
||||||
|
const {messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client} = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
||||||
|
formNode={
|
||||||
|
<div id="kc-info-message">
|
||||||
|
<p className="instruction">
|
||||||
|
{message.summary}
|
||||||
|
|
||||||
|
{requiredActions !== undefined && (
|
||||||
|
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{!skipLink && pageRedirectUri !== undefined ? (
|
||||||
|
<p>
|
||||||
|
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
|
||||||
|
</p>
|
||||||
|
) : actionUri !== undefined ? (
|
||||||
|
<p>
|
||||||
|
<a href={actionUri}>{msg("proceedWithAction")}</a>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
client.baseUrl !== undefined && (
|
||||||
|
<p>
|
||||||
|
<a href={client.baseUrl}>{msg("backToApplication")}</a>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
27
src/keycloak-theme/pages/Login.stories.tsx
Normal file
27
src/keycloak-theme/pages/Login.stories.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { socialProviders, template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Login',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
export const WithoutPasswordField = bind({ realm: { password: false } })
|
||||||
|
export const WithoutRegistration = bind({ realm: { registrationAllowed: false } })
|
||||||
|
export const WithoutRememberMe = bind({ realm: { rememberMe: false } })
|
||||||
|
export const WithoutPasswordReset = bind({ realm: { resetPasswordAllowed: false } })
|
||||||
|
export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: false } })
|
||||||
|
export const WithPresetUsername = bind({ login: { username: 'max.mustermann@mail.com' } })
|
||||||
|
export const WithImmutablePresetUsername = bind({
|
||||||
|
login: { username: 'max.mustermann@mail.com' },
|
||||||
|
usernameEditDisabled: true
|
||||||
|
})
|
||||||
|
export const WithSocialProviders = bind({ social: { displayInfo: true, providers: socialProviders } })
|
27
src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
Normal file
27
src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-config-totp.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Actions/Configure TOTP',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
||||||
|
export const WithManualSetUp = bind({ mode: 'manual' })
|
||||||
|
export const WithError = bind({
|
||||||
|
messagesPerField: {
|
||||||
|
get: (fieldName: string) => fieldName === 'totp' ? 'Invalid TOTP' : undefined,
|
||||||
|
exists: (fieldName: string) => fieldName === 'totp',
|
||||||
|
existsError: (fieldName: string) => fieldName === 'totp',
|
||||||
|
printIfExists: <T,>(fieldName: string, x: T) => fieldName === 'totp' ? x : undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
186
src/keycloak-theme/pages/LoginConfigTotp.tsx
Normal file
186
src/keycloak-theme/pages/LoginConfigTotp.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {clsx} from "keycloakify/lib/tools/clsx";
|
||||||
|
import type {PageProps, KcContextBase} from "keycloakify";
|
||||||
|
import type {KcContext} from "../kcContext";
|
||||||
|
import type {I18n} from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
|
||||||
|
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||||
|
|
||||||
|
const {url, isAppInitiatedAction, totp, mode, messagesPerField} = kcContext;
|
||||||
|
|
||||||
|
const {msg, msgStr} = i18n;
|
||||||
|
|
||||||
|
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
|
||||||
|
"HmacSHA1": "SHA1",
|
||||||
|
"HmacSHA256": "SHA256",
|
||||||
|
"HmacSHA512": "SHA512"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||||
|
headerNode={msg("loginTotpTitle")}
|
||||||
|
formNode={
|
||||||
|
<>
|
||||||
|
<ol id="kc-totp-settings">
|
||||||
|
<li>
|
||||||
|
<p>{msg("loginTotpStep1")}</p>
|
||||||
|
|
||||||
|
<ul id="kc-totp-supported-apps">
|
||||||
|
{totp.policy.supportedApplications.map(app => (
|
||||||
|
<li>{app}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{mode && mode == "manual" ? (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<p>{msg("loginTotpManualStep2")}</p>
|
||||||
|
<p>
|
||||||
|
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href={totp.qrUrl} id="mode-barcode">
|
||||||
|
{msg("loginTotpScanBarcode")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>{msg("loginTotpManualStep3")}</p>
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
<li id="kc-totp-type">
|
||||||
|
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
|
||||||
|
</li>
|
||||||
|
<li id="kc-totp-algorithm">
|
||||||
|
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
|
||||||
|
</li>
|
||||||
|
<li id="kc-totp-digits">
|
||||||
|
{msg("loginTotpDigits")}: {totp.policy.digits}
|
||||||
|
</li>
|
||||||
|
{totp.policy.type === "totp" ? (
|
||||||
|
<li id="kc-totp-period">
|
||||||
|
{msg("loginTotpInterval")}: {totp.policy.period}
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<li id="kc-totp-counter">
|
||||||
|
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<li>
|
||||||
|
<p>{msg("loginTotpStep2")}</p>
|
||||||
|
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
|
||||||
|
alt="Figure: Barcode"/>
|
||||||
|
<br/>
|
||||||
|
<p>
|
||||||
|
<a href={totp.manualUrl} id="mode-manual">
|
||||||
|
{msg("loginTotpUnableToScan")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<p>{msg("loginTotpStep3")}</p>
|
||||||
|
<p>{msg("loginTotpStep3DeviceName")}</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<form action={url.loginAction} className={clsx(kcProps.kcFormClass)} id="kc-totp-settings-form"
|
||||||
|
method="post">
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<label htmlFor="totp" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("authenticatorCode")}
|
||||||
|
</label>{" "}
|
||||||
|
<span className="required">*</span>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="totp"
|
||||||
|
name="totp"
|
||||||
|
autoComplete="off"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
aria-invalid={messagesPerField.existsError("totp")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{messagesPerField.existsError("totp") && (
|
||||||
|
<span id="input-error-otp-code" className={clsx(kcProps.kcInputErrorMessageClass)}
|
||||||
|
aria-live="polite">
|
||||||
|
{messagesPerField.get("totp")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret}/>
|
||||||
|
{mode && <input type="hidden" id="mode" value={mode}/>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<label htmlFor="userLabel" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("loginTotpDeviceName")}
|
||||||
|
</label>{" "}
|
||||||
|
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="userLabel"
|
||||||
|
name="userLabel"
|
||||||
|
autoComplete="off"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||||
|
/>
|
||||||
|
{messagesPerField.existsError("userLabel") && (
|
||||||
|
<span id="input-error-otp-label" className={clsx(kcProps.kcInputErrorMessageClass)}
|
||||||
|
aria-live="polite">
|
||||||
|
{messagesPerField.get("userLabel")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||||
|
id="saveTOTPBtn"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonDefaultClass,
|
||||||
|
kcProps.kcButtonLargeClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
id="cancelTOTPBtn"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
${msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||||
|
id="saveTOTPBtn"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-idp-link-confirm.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/IDP/Confirm Link',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
54
src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx
Normal file
54
src/keycloak-theme/pages/LoginIdpLinkConfirm.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {clsx} from "keycloakify/lib/tools/clsx";
|
||||||
|
import type {PageProps} from "keycloakify";
|
||||||
|
import type {KcContext} from "../kcContext";
|
||||||
|
import type {I18n} from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
|
||||||
|
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||||
|
|
||||||
|
const {url, idpAlias} = kcContext;
|
||||||
|
|
||||||
|
const {msg} = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||||
|
headerNode={msg("confirmLinkIdpTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonDefaultClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
id="updateProfile"
|
||||||
|
value="updateProfile"
|
||||||
|
>
|
||||||
|
{msg("confirmLinkIdpReviewProfile")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonDefaultClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
id="linkAccount"
|
||||||
|
value="linkAccount"
|
||||||
|
>
|
||||||
|
{msg("confirmLinkIdpContinue", idpAlias)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-idp-link-email.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/IDP/Confirm Email',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
32
src/keycloak-theme/pages/LoginIdpLinkEmail.tsx
Normal file
32
src/keycloak-theme/pages/LoginIdpLinkEmail.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type {PageProps} from "keycloakify";
|
||||||
|
import type {KcContext} from "../kcContext";
|
||||||
|
import type {I18n} from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
|
||||||
|
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props;
|
||||||
|
|
||||||
|
const {url, realm, brokerContext, idpAlias} = kcContext;
|
||||||
|
|
||||||
|
const {msg} = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}}
|
||||||
|
headerNode={msg("emailLinkIdpTitle", idpAlias)}
|
||||||
|
formNode={
|
||||||
|
<>
|
||||||
|
<p id="instruction1" className="instruction">
|
||||||
|
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||||
|
</p>
|
||||||
|
<p id="instruction2" className="instruction">
|
||||||
|
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
|
||||||
|
</p>
|
||||||
|
<p id="instruction3" className="instruction">
|
||||||
|
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/LoginOtp.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginOtp.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-otp.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/OTP',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
114
src/keycloak-theme/pages/LoginOtp.tsx
Normal file
114
src/keycloak-theme/pages/LoginOtp.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { headInsert } from "keycloakify/lib/tools/headInsert";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { otpLogin, url } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCleanedUp = false;
|
||||||
|
|
||||||
|
headInsert({
|
||||||
|
"type": "javascript",
|
||||||
|
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
|
||||||
|
}).then(() => {
|
||||||
|
if (isCleanedUp) return;
|
||||||
|
|
||||||
|
evaluateInlineScript();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCleanedUp = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("doLogIn")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-otp-login-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
{otpLogin.userOtpCredentials.length > 1 && (
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
{otpLogin.userOtpCredentials.map(otpCredential => (
|
||||||
|
<div key={otpCredential.id} className={clsx(kcProps.kcSelectOTPListClass)}>
|
||||||
|
<input type="hidden" value="${otpCredential.id}" />
|
||||||
|
<div className={clsx(kcProps.kcSelectOTPListItemClass)}>
|
||||||
|
<span className={clsx(kcProps.kcAuthenticatorOtpCircleClass)} />
|
||||||
|
<h2 className={clsx(kcProps.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="otp" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("loginOtpOneTime")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input id="otp" name="otp" autoComplete="off" type="text" className={clsx(kcProps.kcInputClass)} autoFocus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="login"
|
||||||
|
id="kc-login"
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doLogIn")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const $: any;
|
||||||
|
|
||||||
|
function evaluateInlineScript() {
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Card Single Select
|
||||||
|
$(".card-pf-view-single-select").click(function (this: any) {
|
||||||
|
if ($(this).hasClass("active")) {
|
||||||
|
$(this).removeClass("active");
|
||||||
|
$(this).children().removeAttr("name");
|
||||||
|
} else {
|
||||||
|
$(".card-pf-view-single-select").removeClass("active");
|
||||||
|
$(".card-pf-view-single-select").children().removeAttr("name");
|
||||||
|
$(this).addClass("active");
|
||||||
|
$(this).children().attr("name", "selectedCredentialId");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var defaultCred = $(".card-pf-view-single-select")[0];
|
||||||
|
if (defaultCred) {
|
||||||
|
defaultCred.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
16
src/keycloak-theme/pages/LoginPageExpired.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginPageExpired.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-page-expired.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Login Page Expired',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
36
src/keycloak-theme/pages/LoginPageExpired.tsx
Normal file
36
src/keycloak-theme/pages/LoginPageExpired.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { url } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("pageExpiredTitle")}
|
||||||
|
formNode={
|
||||||
|
<>
|
||||||
|
<p id="instruction1" className="instruction">
|
||||||
|
{msg("pageExpiredMsg1")}
|
||||||
|
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||||
|
{msg("doClickHere")}
|
||||||
|
</a>{" "}
|
||||||
|
.<br />
|
||||||
|
{msg("pageExpiredMsg2")}{" "}
|
||||||
|
<a id="loginContinueLink" href={url.loginAction}>
|
||||||
|
{msg("doClickHere")}
|
||||||
|
</a>{" "}
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/LoginPassword.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginPassword.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-password.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Password',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
86
src/keycloak-theme/pages/LoginPassword.tsx
Normal file
86
src/keycloak-theme/pages/LoginPassword.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import { useConstCallback } from "keycloakify/lib/tools/useConstCallback";
|
||||||
|
import type { FormEventHandler } from "react";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginPassword(props: PageProps<Extract<KcContext, { "pageId": "login-password.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { realm, url, login } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsLoginButtonDisabled(true);
|
||||||
|
|
||||||
|
const formElement = e.target as HTMLFormElement;
|
||||||
|
|
||||||
|
formElement.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("doLogIn")}
|
||||||
|
formNode={
|
||||||
|
<div id="kc-form">
|
||||||
|
<div id="kc-form-wrapper">
|
||||||
|
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<hr />
|
||||||
|
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("password")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
tabIndex={2}
|
||||||
|
id="password"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoFocus={true}
|
||||||
|
autoComplete="on"
|
||||||
|
defaultValue={login.password ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||||
|
<div id="kc-form-options" />
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||||
|
{realm.resetPasswordAllowed && (
|
||||||
|
<span>
|
||||||
|
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
||||||
|
{msg("doForgotPassword")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<input
|
||||||
|
tabIndex={4}
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="login"
|
||||||
|
id="kc-login"
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doLogIn")}
|
||||||
|
disabled={isLoginButtonDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
src/keycloak-theme/pages/LoginResetPassword.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginResetPassword.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-reset-password.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Reset Password',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true } })
|
69
src/keycloak-theme/pages/LoginResetPassword.tsx
Normal file
69
src/keycloak-theme/pages/LoginResetPassword.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginResetPassword(props: PageProps<Extract<KcContext, { pageId: "login-reset-password.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { url, realm, auth } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("emailForgotTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-reset-password-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{!realm.loginWithEmailAllowed
|
||||||
|
? msg("username")
|
||||||
|
: !realm.registrationEmailAsUsername
|
||||||
|
? msg("usernameOrEmail")
|
||||||
|
: msg("email")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
autoFocus
|
||||||
|
defaultValue={auth !== undefined && auth.showUsername ? auth.attemptedUsername : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||||
|
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||||
|
<span>
|
||||||
|
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
infoNode={msg("emailInstruction")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-update-password.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Actions/Update Password',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
117
src/keycloak-theme/pages/LoginUpdatePassword.tsx
Normal file
117
src/keycloak-theme/pages/LoginUpdatePassword.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginUpdatePassword(props: PageProps<Extract<KcContext, { pageId: "login-update-password.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("updatePasswordTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-passwd-update-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
value={username}
|
||||||
|
readOnly={true}
|
||||||
|
autoComplete="username"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
<input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}>
|
||||||
|
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="password-new" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("passwordNew")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password-new"
|
||||||
|
name="password-new"
|
||||||
|
autoFocus
|
||||||
|
autoComplete="new-password"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass))}
|
||||||
|
>
|
||||||
|
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg("passwordConfirm")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password-confirm"
|
||||||
|
name="password-confirm"
|
||||||
|
autoComplete="new-password"
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||||
|
{isAppInitiatedAction && (
|
||||||
|
<div className="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
|
||||||
|
{msgStr("logoutOtherSessions")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
src/keycloak-theme/pages/LoginUsername.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginUsername.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-username.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Username',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true } })
|
158
src/keycloak-theme/pages/LoginUsername.tsx
Normal file
158
src/keycloak-theme/pages/LoginUsername.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import { useConstCallback } from "keycloakify/lib/tools/useConstCallback";
|
||||||
|
import type { FormEventHandler } from "react";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginUsername(props: PageProps<Extract<KcContext, { pageId: "login-username.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsLoginButtonDisabled(true);
|
||||||
|
|
||||||
|
const formElement = e.target as HTMLFormElement;
|
||||||
|
|
||||||
|
//NOTE: Even if we login with email Keycloak expect username and password in
|
||||||
|
//the POST request.
|
||||||
|
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
|
||||||
|
|
||||||
|
formElement.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayInfo={social.displayInfo}
|
||||||
|
displayWide={realm.password && social.providers !== undefined}
|
||||||
|
headerNode={msg("doLogIn")}
|
||||||
|
formNode={
|
||||||
|
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
|
||||||
|
<div
|
||||||
|
id="kc-form-wrapper"
|
||||||
|
className={clsx(
|
||||||
|
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{realm.password && (
|
||||||
|
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
{!usernameHidden &&
|
||||||
|
(() => {
|
||||||
|
const label = !realm.loginWithEmailAllowed
|
||||||
|
? "username"
|
||||||
|
: realm.registrationEmailAsUsername
|
||||||
|
? "email"
|
||||||
|
: "usernameOrEmail";
|
||||||
|
|
||||||
|
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}>
|
||||||
|
{msg(label)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
tabIndex={1}
|
||||||
|
id={autoCompleteHelper}
|
||||||
|
className={clsx(kcProps.kcInputClass)}
|
||||||
|
// NOTE: This is used by Google Chrome auto fill so we use it to tell
|
||||||
|
// the browser how to pre fill the form but before submit we put it back
|
||||||
|
// to username because it is what keycloak expects.
|
||||||
|
name={autoCompleteHelper}
|
||||||
|
defaultValue={login.username ?? ""}
|
||||||
|
type="text"
|
||||||
|
autoFocus={true}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||||
|
<div id="kc-form-options">
|
||||||
|
{realm.rememberMe && !usernameHidden && (
|
||||||
|
<div className="checkbox">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
tabIndex={3}
|
||||||
|
id="rememberMe"
|
||||||
|
name="rememberMe"
|
||||||
|
type="checkbox"
|
||||||
|
{...(login.rememberMe
|
||||||
|
? {
|
||||||
|
"checked": true
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
/>
|
||||||
|
{msg("rememberMe")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<input
|
||||||
|
tabIndex={4}
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="login"
|
||||||
|
id="kc-login"
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doLogIn")}
|
||||||
|
disabled={isLoginButtonDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{realm.password && social.providers !== undefined && (
|
||||||
|
<div id="kc-social-providers" className={clsx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
|
||||||
|
<ul
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcFormSocialAccountListClass,
|
||||||
|
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{social.providers.map(p => (
|
||||||
|
<li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}>
|
||||||
|
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
|
||||||
|
<span>{p.displayName}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
infoNode={
|
||||||
|
realm.password &&
|
||||||
|
realm.registrationAllowed &&
|
||||||
|
!registrationDisabled && (
|
||||||
|
<div id="kc-registration">
|
||||||
|
<span>
|
||||||
|
{msg("noAccount")}
|
||||||
|
<a tabIndex={6} href={url.registrationUrl}>
|
||||||
|
{msg("doRegister")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('login-verify-email.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Verify Email',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
32
src/keycloak-theme/pages/LoginVerifyEmail.tsx
Normal file
32
src/keycloak-theme/pages/LoginVerifyEmail.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { PageProps } from "keycloakify";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LoginVerifyEmail(props: PageProps<Extract<KcContext, { pageId: "login-verify-email.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
const { url, user } = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("emailVerifyTitle")}
|
||||||
|
formNode={
|
||||||
|
<>
|
||||||
|
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
|
||||||
|
<p className="instruction">
|
||||||
|
{msg("emailVerifyInstruction2")}
|
||||||
|
<br />
|
||||||
|
<a href={url.loginAction}>{msg("doClickHere")}</a>
|
||||||
|
|
||||||
|
{msg("emailVerifyInstruction3")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/LogoutConfirm.stories.tsx
Normal file
16
src/keycloak-theme/pages/LogoutConfirm.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('logout-confirm.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Logout Confirmation',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
58
src/keycloak-theme/pages/LogoutConfirm.tsx
Normal file
58
src/keycloak-theme/pages/LogoutConfirm.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "logout-confirm.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { url, client, logoutConfirm } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("logoutConfirmTitle")}
|
||||||
|
formNode={
|
||||||
|
<>
|
||||||
|
<div id="kc-logout-confirm" className="content-area">
|
||||||
|
<p className="instruction">{msg("logoutConfirmHeader")}</p>
|
||||||
|
<form className="form-actions" action={url.logoutConfirmAction} method="POST">
|
||||||
|
<input type="hidden" name="session_code" value={logoutConfirm.code} />
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options">
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<input
|
||||||
|
tabIndex={4}
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="confirmLogout"
|
||||||
|
id="kc-logout"
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doLogout")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="kc-info-message">
|
||||||
|
{!logoutConfirm.skipLink && client.baseUrl && (
|
||||||
|
<p>
|
||||||
|
<a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/MyExtraPage1.stories.tsx
Normal file
16
src/keycloak-theme/pages/MyExtraPage1.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('my-extra-page-1.ftl')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Custom/My Extra Page 1',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
18
src/keycloak-theme/pages/MyExtraPage2.stories.tsx
Normal file
18
src/keycloak-theme/pages/MyExtraPage2.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('my-extra-page-2.ftl')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Custom/My Extra Page 2',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
||||||
|
export const WithCustomValue = bind({ someCustomValue: 'Foo Bar Baz' })
|
54
src/keycloak-theme/pages/Register.stories.tsx
Normal file
54
src/keycloak-theme/pages/Register.stories.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('register.ftl')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Register/Legacy',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
||||||
|
export const WithFieldError = bind({
|
||||||
|
register: {
|
||||||
|
formData: {
|
||||||
|
email: 'max.mustermann@mail.com'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messagesPerField: {
|
||||||
|
existsError: (fieldName: string) => fieldName === "email",
|
||||||
|
exists: (fieldName: string) => fieldName === "email",
|
||||||
|
get: (fieldName: string) => fieldName === "email" ? "I don't like your email address" : undefined,
|
||||||
|
printIfExists: <T,>(fieldName: string, x: T) => fieldName === "email" ? x : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithEmailAsUsername = bind({
|
||||||
|
realm: { registrationEmailAsUsername: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithoutPassword = bind({
|
||||||
|
passwordRequired: false
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithRecaptcha = bind({
|
||||||
|
recaptchaRequired: true,
|
||||||
|
recaptchaSiteKey: 'foobar'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithPresets = bind({
|
||||||
|
register: {
|
||||||
|
formData: {
|
||||||
|
firstName: 'Max',
|
||||||
|
lastName: 'Mustermann',
|
||||||
|
email: 'max.mustermann@mail.com',
|
||||||
|
username: 'max.mustermann'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -54,6 +54,7 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
name="lastName"
|
name="lastName"
|
||||||
defaultValue={register.formData.lastName ?? ""}
|
defaultValue={register.formData.lastName ?? ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
82
src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
Normal file
82
src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('register-user-profile.ftl')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Register/Modern',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: { layout: 'fullscreen' },
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
||||||
|
|
||||||
|
/*
|
||||||
|
export const WithFieldError = bind({
|
||||||
|
profile: {
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
value: "max.mustermann@mail.com",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithPresets = bind({
|
||||||
|
profile: {
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "username",
|
||||||
|
value: "max.mustermann"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
value: "max.mustermann@gmail.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "firstName",
|
||||||
|
required: false,
|
||||||
|
value: "Max"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lastName",
|
||||||
|
required: false,
|
||||||
|
value: "Mustermann"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const WithImmutablePresets = bind({
|
||||||
|
profile: {
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "username",
|
||||||
|
value: "max.mustermann",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
value: "max.mustermann@gmail.com",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "firstName",
|
||||||
|
required: true,
|
||||||
|
value: "Max",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lastName",
|
||||||
|
required: true,
|
||||||
|
value: "Mustermann",
|
||||||
|
readOnly: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
*/
|
@ -1,7 +1,7 @@
|
|||||||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
|
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
|
||||||
import type { PageProps } from "keycloakify/lib/KcProps";
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../kcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
16
src/keycloak-theme/pages/Terms.stories.tsx
Normal file
16
src/keycloak-theme/pages/Terms.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('terms.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Actions/Terms',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* NOTE: Yo do not need to do all this to put your own Terms and conditions
|
* NOTE: You do not need to do all this to put your own Terms and conditions
|
||||||
* this is if you want component level customization.
|
* this is if you want component level customization.
|
||||||
* If the default works for you you can just use the useDownloadTerms hook
|
* If the default works for you, you can just use the useDownloadTerms hook
|
||||||
* in the KcApp.tsx
|
* in the KcApp.tsx
|
||||||
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
|
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
|
||||||
*/
|
*/
|
||||||
@ -24,14 +24,20 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
|
|||||||
kcContext,
|
kcContext,
|
||||||
"downloadTermMarkdown": async ({currentLanguageTag}) => {
|
"downloadTermMarkdown": async ({currentLanguageTag}) => {
|
||||||
|
|
||||||
const markdownString = await fetch((() => {
|
const resource = (() => {
|
||||||
switch (currentLanguageTag) {
|
switch (currentLanguageTag) {
|
||||||
case "fr": return tos_fr_url;
|
case "fr":
|
||||||
default: return tos_en_url;
|
return tos_fr_url;
|
||||||
|
default:
|
||||||
|
return tos_en_url;
|
||||||
}
|
}
|
||||||
})()).then(response => response.text());
|
})();
|
||||||
|
|
||||||
return markdownString;
|
// webpack5 (used via storybook) loads markdown as string, not url
|
||||||
|
if (resource.includes("\n")) return resource
|
||||||
|
|
||||||
|
const response = await fetch(resource);
|
||||||
|
return response.text();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,7 +56,8 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
|
|||||||
headerNode={msg("termsTitle")}
|
headerNode={msg("termsTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
<>
|
<>
|
||||||
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
<div id="kc-terms-text">{evtTermMarkdown.state &&
|
||||||
|
<Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||||
<form className="form-actions" action={url.loginAction} method="POST">
|
<form className="form-actions" action={url.loginAction} method="POST">
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
16
src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
Normal file
16
src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('update-user-profile.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Actions/Update User Profile',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
67
src/keycloak-theme/pages/UpdateUserProfile.tsx
Normal file
67
src/keycloak-theme/pages/UpdateUserProfile.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function UpdateUserProfile(props: PageProps<Extract<KcContext, { pageId: "update-user-profile.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("loginProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-update-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||||
|
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
formNoValidate
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
16
src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
Normal file
16
src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ComponentMeta } from '@storybook/react';
|
||||||
|
import KcApp from '../KcApp';
|
||||||
|
import { template } from '../../../.storybook/util'
|
||||||
|
|
||||||
|
const bind = template('webauthn-authenticate.ftl');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kind: 'Page',
|
||||||
|
title: 'Theme/Pages/Login/Webauthn',
|
||||||
|
component: KcApp,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof KcApp>;
|
||||||
|
|
||||||
|
export const Default = bind({})
|
193
src/keycloak-theme/pages/WebauthnAuthenticate.tsx
Normal file
193
src/keycloak-theme/pages/WebauthnAuthenticate.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { MessageKeyBase } from "keycloakify/lib/i18n";
|
||||||
|
import { base64url } from "rfc4648";
|
||||||
|
import { useConstCallback } from "keycloakify/lib/tools/useConstCallback";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext, { pageId: "webauthn-authenticate.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { url } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext;
|
||||||
|
const createTimeout = Number(kcContext.createTimeout);
|
||||||
|
const isUserIdentified = kcContext.isUserIdentified == "true";
|
||||||
|
|
||||||
|
const webAuthnAuthenticate = useConstCallback(async () => {
|
||||||
|
if (!isUserIdentified) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowCredentials = authenticators.authenticators.map(
|
||||||
|
authenticator =>
|
||||||
|
({
|
||||||
|
id: base64url.parse(authenticator.credentialId, { loose: true }),
|
||||||
|
type: "public-key"
|
||||||
|
} as PublicKeyCredentialDescriptor)
|
||||||
|
);
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
setError(msgStr("webauthn-unsupported-browser-text"));
|
||||||
|
submitForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey: PublicKeyCredentialRequestOptions = {
|
||||||
|
rpId,
|
||||||
|
challenge: base64url.parse(challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createTimeout !== 0) {
|
||||||
|
publicKey.timeout = createTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowCredentials.length) {
|
||||||
|
publicKey.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userVerification !== "not specified") {
|
||||||
|
publicKey.userVerification = userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultRaw = await navigator.credentials.get({ publicKey });
|
||||||
|
if (!resultRaw || resultRaw.type != "public-key") return;
|
||||||
|
const result = resultRaw as PublicKeyCredential;
|
||||||
|
if (!("authenticatorData" in result.response)) return;
|
||||||
|
const response = result.response as AuthenticatorAssertionResponse;
|
||||||
|
const clientDataJSON = response.clientDataJSON;
|
||||||
|
const authenticatorData = response.authenticatorData;
|
||||||
|
const signature = response.signature;
|
||||||
|
|
||||||
|
setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { pad: false }));
|
||||||
|
setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { pad: false }));
|
||||||
|
setSignature(base64url.stringify(new Uint8Array(signature), { pad: false }));
|
||||||
|
setCredentialId(result.id);
|
||||||
|
setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { pad: false }));
|
||||||
|
submitForm();
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
submitForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const webAuthForm = useRef<HTMLFormElement>(null);
|
||||||
|
const submitForm = useConstCallback(() => {
|
||||||
|
webAuthForm.current!.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [clientDataJSON, setClientDataJSON] = useState("");
|
||||||
|
const [authenticatorData, setAuthenticatorData] = useState("");
|
||||||
|
const [signature, setSignature] = useState("");
|
||||||
|
const [credentialId, setCredentialId] = useState("");
|
||||||
|
const [userHandle, setUserHandle] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
headerNode={msg("webauthn-login-title")}
|
||||||
|
formNode={
|
||||||
|
<div id="kc-form-webauthn" className={clsx(kcProps.kcFormClass)}>
|
||||||
|
<form id="webauth" action={url.loginAction} ref={webAuthForm} method="post">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON" value={clientDataJSON} />
|
||||||
|
<input type="hidden" id="authenticatorData" name="authenticatorData" value={authenticatorData} />
|
||||||
|
<input type="hidden" id="signature" name="signature" value={signature} />
|
||||||
|
<input type="hidden" id="credentialId" name="credentialId" value={credentialId} />
|
||||||
|
<input type="hidden" id="userHandle" name="userHandle" value={userHandle} />
|
||||||
|
<input type="hidden" id="error" name="error" value={error} />
|
||||||
|
</form>
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||||
|
{authenticators &&
|
||||||
|
(() => (
|
||||||
|
<form id="authn_select" className={clsx(kcProps.kcFormClass)}>
|
||||||
|
{authenticators.authenticators.map(authenticator => (
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authn_use_chk"
|
||||||
|
value={authenticator.credentialId}
|
||||||
|
key={authenticator.credentialId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
))()}
|
||||||
|
{authenticators &&
|
||||||
|
shouldDisplayAuthenticators &&
|
||||||
|
(() => (
|
||||||
|
<>
|
||||||
|
{authenticators.authenticators.length > 1 && (
|
||||||
|
<p className={clsx(kcProps.kcSelectAuthListItemTitle)}>{msg("webauthn-available-authenticators")}</p>
|
||||||
|
)}
|
||||||
|
<div className={clsx(kcProps.kcFormClass)}>
|
||||||
|
{authenticators.authenticators.map(authenticator => (
|
||||||
|
<div id="kc-webauthn-authenticator" className={clsx(kcProps.kcSelectAuthListItemClass)}>
|
||||||
|
<div className={clsx(kcProps.kcSelectAuthListItemIconClass)}>
|
||||||
|
<i
|
||||||
|
className={clsx(
|
||||||
|
kcProps[authenticator.transports.iconClass] ?? kcProps.kcWebAuthnDefaultIcon,
|
||||||
|
kcProps.kcSelectAuthListItemIconPropertyClass
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcSelectAuthListItemBodyClass)}>
|
||||||
|
<div
|
||||||
|
id="kc-webauthn-authenticator-label"
|
||||||
|
className={clsx(kcProps.kcSelectAuthListItemHeadingClass)}
|
||||||
|
>
|
||||||
|
{authenticator.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{authenticator.transports && authenticator.transports.displayNameProperties.length && (
|
||||||
|
<div
|
||||||
|
id="kc-webauthn-authenticator-transport"
|
||||||
|
className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}
|
||||||
|
>
|
||||||
|
{authenticator.transports.displayNameProperties.map(
|
||||||
|
(transport: MessageKeyBase, index: number) => (
|
||||||
|
<>
|
||||||
|
<span>{msg(transport)}</span>
|
||||||
|
{index < authenticator.transports.displayNameProperties.length - 1 && (
|
||||||
|
<span>{", "}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}>
|
||||||
|
<span id="kc-webauthn-authenticator-created-label">{msg("webauthn-createdAt-label")}</span>
|
||||||
|
<span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(kcProps.kcSelectAuthListItemFillClass)} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))()}
|
||||||
|
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
id="authenticateWebAuthnButton"
|
||||||
|
type="button"
|
||||||
|
onClick={webAuthnAuthenticate}
|
||||||
|
autoFocus={true}
|
||||||
|
value={msgStr("webauthn-doAuthenticate")}
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user