commit
01961cae02
|
@ -1,23 +1,23 @@
|
|||
{
|
||||
"name": "Keycloakify Starter Devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": true,
|
||||
"installDockerBuildx": true,
|
||||
"version": "latest",
|
||||
"dockerDashComposeVersion": "none"
|
||||
"name": "Keycloakify Starter Devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": true,
|
||||
"installDockerBuildx": true,
|
||||
"version": "latest",
|
||||
"dockerDashComposeVersion": "none"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/maven-sdkman:2": {
|
||||
"version": "latest",
|
||||
"jdkVersion": "latest",
|
||||
"jdkDistro": "ms"
|
||||
}
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/maven-sdkman:2": {
|
||||
"version": "latest",
|
||||
"jdkVersion": "latest",
|
||||
"jdkDistro": "ms"
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
/Dockerfile
|
||||
/node_modules
|
||||
/.github
|
||||
/.vscode
|
||||
/docs
|
||||
/build
|
|
@ -1,25 +1,27 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'off',
|
||||
'no-labels': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.stories.*'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
},
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react-refresh"],
|
||||
rules: {
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-redeclare": "off",
|
||||
"no-labels": "off"
|
||||
},
|
||||
],
|
||||
}
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.stories.*"],
|
||||
rules: {
|
||||
"import/no-anonymous-default-export": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
150
.github/workflows/ci.yaml
vendored
150
.github/workflows/ci.yaml
vendored
|
@ -1,109 +1,53 @@
|
|||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run build-keycloak-theme
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx keycloakify
|
||||
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
outputs:
|
||||
from_version: ${{ steps.step1.outputs.from_version }}
|
||||
to_version: ${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
branch: ${{ github.head_ref || github.ref }}
|
||||
|
||||
create_github_release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_if_version_upgraded
|
||||
# We create a release only if the version have been upgraded and we are on a default branch
|
||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' && github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx keycloakify
|
||||
- run: mv dist_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar
|
||||
- run: mv dist_keycloak/target/*.jar keycloak-theme.jar
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
target_commitish: ${{ github.head_ref || github.ref }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
files: |
|
||||
retrocompat-keycloak-theme.jar
|
||||
keycloak-theme.jar
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check_if_version_upgraded
|
||||
- create_github_release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Computing Docker image tags
|
||||
id: step1
|
||||
env:
|
||||
IS_UPGRADED_VERSION: ${{ needs.check_if_version_upgraded.outputs.is_upgraded_version }}
|
||||
TO_VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
run: |
|
||||
OUT=$GITHUB_REPOSITORY:$TO_VERSION,$GITHUB_REPOSITORY:latest
|
||||
OUT=$(echo "$OUT" | awk '{print tolower($0)}')
|
||||
echo ::set-output name=docker_tags::$OUT
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
tags: ${{ steps.step1.outputs.docker_tags }}
|
||||
|
||||
github_pages:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- create_github_release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
# We tell GitHub pages that our package.json["homepage"] field is our domain name.
|
||||
# If you wish to use the default GitHub pages domain name, like https://<username>.github.io/<repo>,
|
||||
# you'll have to use base: "/repo/" in your vite.config.ts.
|
||||
- run: echo $(node -e 'console.log(require("url").parse(require("./package.json").homepage).host)') > dist/CNAME
|
||||
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
- run: npx -y -p gh-pages@3.0.0 gh-pages -u "github-actions-bot <actions@github.com>" -d dist
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
outputs:
|
||||
from_version: ${{ steps.step1.outputs.from_version }}
|
||||
to_version: ${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
branch: ${{ github.head_ref || github.ref }}
|
||||
|
||||
create_github_release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_if_version_upgraded
|
||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run build-keycloak-theme
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
target_commitish: ${{ github.head_ref || github.ref }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
files: dist_keycloak/keycloak-theme-*.jar
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
6
.prettierignore
Normal file
6
.prettierignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules/
|
||||
/dist/
|
||||
/dist_keycloak/
|
||||
/public/keycloak-resources/
|
||||
/.vscode/
|
||||
/.yarn_home/
|
25
.prettierrc.json
Normal file
25
.prettierrc.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"printWidth": 90,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/login/pages/*.tsx",
|
||||
"**/account/pages/*.tsx",
|
||||
"**/login/Template.tsx",
|
||||
"**/account/Template.tsx",
|
||||
"**/login/UserProfileFormFields.tsx",
|
||||
"KcApp.tsx"
|
||||
],
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,20 +1,12 @@
|
|||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
staticDirs: ["../public"]
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {}
|
||||
},
|
||||
staticDirs: ["../public"]
|
||||
};
|
||||
export default config;
|
||||
|
|
25
.storybook/preview-head.html
Normal file
25
.storybook/preview-head.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<style>
|
||||
body.sb-show-main.sb-main-padded {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Following styles are just meant to avoid white flash when switching from one story to another */
|
||||
@keyframes fadeToTransparent {
|
||||
from {
|
||||
background-color: #393939;
|
||||
}
|
||||
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
html {
|
||||
animation: fadeToTransparent 500ms forwards ease-in;
|
||||
}
|
||||
body > .sb-preparing-docs {
|
||||
visibility: hidden;
|
||||
}
|
||||
body > .sb-preparing-story {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
|
@ -1,14 +1,14 @@
|
|||
import type { Preview } from "@storybook/react";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
|
13
Dockerfile
13
Dockerfile
|
@ -1,13 +0,0 @@
|
|||
# build environment
|
||||
FROM node:18-alpine as build
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
|
||||
# production environment
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
CMD nginx -g 'daemon off;'
|
224
README.md
224
README.md
|
@ -1,229 +1,47 @@
|
|||
<p align="center">
|
||||
<i>🚀 A starter/demo project for <a href="https://keycloakify.dev">Keycloakify</a> v9 🚀</i>
|
||||
<i>🚀 <a href="https://keycloakify.dev">Keycloakify</a> v10 starter 🚀</i>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://github.com/codegouvfr/keycloakify-starter/workflows/ci/badge.svg?branch=main">
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://starter.keycloakify.dev">Authenticated React SPA</a>
|
||||
</p>
|
||||
|
||||
# Introduction
|
||||
|
||||
This repo constitutes an easily reusable setup for a Keycloak theme project OR for a Vite SPA React App that generates a
|
||||
Keycloak theme that goes along with it.
|
||||
If you are only looking to create a Keycloak theme (and not a Keycloak theme and an App that share the same codebase) there are a lot of things that you can remove from this starter: [Please read this section of the README](#i-only-want-a-keycloak-theme).
|
||||
|
||||
This starter is based on Vite. There is also [a Webpack based starter](https://github.com/keycloakify/keycloakify-starter-cra).
|
||||
|
||||
> 📣 Looking for a library for redirecting your user to Keycloak when they click on the 'Login' button?
|
||||
> Check out [oidc-spa](https://oidc-spa.dev) It's made by us and it's used in the [src/App](https://github.com/keycloakify/keycloakify-starter/tree/main/src/App) of this starter.
|
||||
This starter is based on Vite. There is also [a Webpack based starter](https://github.com/keycloakify/keycloakify-starter-webpack).
|
||||
|
||||
# Quick start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/keycloakify/keycloakify-starter
|
||||
|
||||
cd keycloakify-starter
|
||||
|
||||
yarn # install dependencies (it's like npm install)
|
||||
|
||||
yarn storybook # Start Storybook
|
||||
# This is by far the best way to develop your theme
|
||||
# This enable to quickly see your pages in isolation and in different states.
|
||||
# You can create stories even for pages that you haven't explicitly overloaded. See src/keycloak-theme/login/pages/LoginResetPassword.stories.tsx
|
||||
# See Keycloakify's storybook for if you need a starting point for your stories: https://github.com/keycloakify/keycloakify/tree/main/stories
|
||||
|
||||
yarn dev # See the Hello World app
|
||||
# Uncomment line 97 of src/keycloak-theme/login/kcContext where it reads: `mockPageId: "login.ftl"`, reload https://localhost:3000
|
||||
# You can now see the login.ftl page with the mock data. (Don't forget to comment it back when you're done)
|
||||
|
||||
# Install mvn (Maven) if not already done. On mac it's 'brew install maven', on Ubuntu/Debian it's 'sudo apt-get install maven'
|
||||
|
||||
yarn build-keycloak-theme # Actually build the theme (generates the .jar to be imported in Keycloak)
|
||||
# Read the instruction printed on the console to see how to test
|
||||
# your theme on a real Keycloak instance.
|
||||
|
||||
npx eject-keycloak-page # Prompt that let you select the pages you want to customize
|
||||
# This CLI tools is not guaranty to work, you can always copy pase pages
|
||||
# from the Keycloakify repo.
|
||||
# After you ejected a page you need to edit the src/keycloak-theme/login(or admin)/KcApp.tsx file
|
||||
# You need to add a case in the switch for the page you just imported in your project.
|
||||
# Look how it's done for the Login page and replicate for your new page.
|
||||
|
||||
npx initialize-email-theme # For initializing your email theme
|
||||
# Note that Keycloakify does not feature React integration for email yet.
|
||||
|
||||
npx download-builtin-keycloak-theme # For downloading the default theme (as a reference)
|
||||
# Look for the files in dist_keycloak/src/main/resources/theme/{base,keycloak}
|
||||
yarn install # Or use an other package manager, just be sure to delete the yarn.lock if you do.
|
||||
```
|
||||
|
||||
## Using a development container
|
||||
# Testing the theme locally
|
||||
|
||||
This starter supports [development containers](https://containers.dev/). You can customize the configuration file [`.devcontainer.json`](./.devcontainer/devcontainer.json) to your liking.
|
||||
Checkout [this video](https://www.youtube.com/watch?v=cB86HE_HIDc) to understand dev containers and how to set up your environment.
|
||||
[Documentation](https://docs.keycloakify.dev/v/v10/testing-your-theme)
|
||||
|
||||
# Theme variant
|
||||
# How to customize the theme
|
||||
|
||||
Keycloakify enables you to create different variant for a single theme.
|
||||
This enable you to have a single jar that embed two or more theme variant.
|
||||
[Documentation](https://docs.keycloakify.dev/v/v10/customization-strategies)
|
||||
|
||||
![Theme variant](https://content.gitbook.com/content/FcBKODbZbNDgm0rc6a9K/blobs/9iKgs2rv2Kfb2pbs4dRz/image.png)
|
||||
# Building the theme
|
||||
|
||||
You can enable this feature by providing multiple theme name in the Keycloakify build option.
|
||||
[See documentation](https://docs.keycloakify.dev/build-options#themename)
|
||||
|
||||
# The CI workflow
|
||||
|
||||
- To release **don't create a tag manually**, the CI do it for you. Just update the `package.json`'s version field and push.
|
||||
- The `.jar` files that bundle the Keycloak theme will be attached as an asset with every GitHub release. [Example](https://github.com/InseeFrLab/keycloakify-starter/releases/tag/v0.1.0). The permalink to download the latest version is: `https://github.com/USER/PROJECT/releases/latest/download/keycloak-theme.jar`.
|
||||
For this demo repo it's [here](https://github.com/codegouvfr/keycloakify-starter/releases/latest/download/keycloak-theme.jar)
|
||||
- This CI is configured to publish [the app](https://starter.keycloakify.dev) on [GitHub Pages](https://github.com/codegouvfr/keycloakify-starter/blob/3617a71deb1a6544c3584aa8d6d2241647abd48c/.github/workflows/ci.yaml#L51-L76) and on [DockerHub](https://github.com/codegouvfr/keycloakify-starter/blob/3617a71deb1a6544c3584aa8d6d2241647abd48c/.github/workflows/ci.yaml#L78-L123) (as a Ngnix based docker image). In practice you probably want one or the other but not both... or neither if you are just building a theme (and not a theme + an app).
|
||||
If you want to enable the CI to publish on DockerHub on your behalf go to repository `Settings` tab, then `Secrets` you will need to add two new secrets:
|
||||
`DOCKERHUB_TOKEN`, you Dockerhub authorization token.
|
||||
`DOCKERHUB_USERNAME`, Your Dockerhub username.
|
||||
We deploy the demo app at [starter.keycloakify.dev](https://starter.keycloakify.dev) using GitHub page on the branch `gh-pages` (you have to enable it).
|
||||
To configure your own domain name update the homepage field of the `package.json` and potentially the `base` option in the `vite.config.ts`.
|
||||
Regarding DNS configuration you can refer to [this documentation](https://docs.gitlanding.dev/using-a-custom-domain-name).
|
||||
- The CI publishes the app docker image on DockerHub. `<org>/<repo>:main` for each **commit** on `main`, `<org>/<repo>:<feature-branch-name>` for each **pull-request** on `main`
|
||||
and when **releasing a new version**: `<org>/<repo>:latest` and `<org>/<repo>:X.Y.Z`
|
||||
[See on DockerHub](https://hub.docker.com/r/codegouvfr/keycloakify-starter)
|
||||
|
||||
![image](https://user-images.githubusercontent.com/6702424/229296422-9d522707-114e-4282-93f7-01ca38c3a1e0.png)
|
||||
|
||||
![image](https://user-images.githubusercontent.com/6702424/229296556-a69f2dc9-4653-475c-9c89-d53cf33dc05a.png)
|
||||
|
||||
|
||||
# The storybook
|
||||
|
||||
![image](https://github.com/keycloakify/keycloakify/assets/6702424/a18ac1ff-dcfd-4b8c-baed-dcda5aa1d762)
|
||||
You need to have Maven installed to build the theme (The `mvn` command must be in the PATH).
|
||||
- On macOS: `brew install maven`
|
||||
- On Debian/Ubuntu: `sudo apt-get install maven`
|
||||
- On Windows: `choco install openjdk` and `choco install maven` (Or download from [here](https://maven.apache.org/download.cgi))
|
||||
|
||||
```bash
|
||||
yarn
|
||||
yarn storybook
|
||||
npm run build-keycloak-theme
|
||||
```
|
||||
|
||||
# Docker
|
||||
Note that by default Keycloakify generates multiple .jar files for different versions of Keycloak.
|
||||
You can customize this behavior, see documentation [here](https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions).
|
||||
|
||||
Instructions for building and running the react app (`src/App`) that is collocated with our Keycloak theme.
|
||||
# GitHub Actions
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile -t keycloakify/keycloakify-starter:main .
|
||||
docker run -it -dp 8083:80 keycloakify/keycloakify-starter:main
|
||||
# You can access the app at http://localhost:8083
|
||||
```
|
||||
The starter comes with a generic GitHub Actions workflow that builds the theme and publishes
|
||||
the jars [as GitHub releases artifacts](https://github.com/keycloakify/keycloakify-starter/releases/tag/v7.1.0).
|
||||
To release a new version **just update the `package.json` version and push**.
|
||||
|
||||
# I only want a Keycloak theme
|
||||
To enable the workflow go to your fork of this repository on GitHub then navigate to:
|
||||
`Settings` > `Actions` > `Workflow permissions`, select `Read and write permissions`.
|
||||
|
||||
If you are only looking to create a Keycloak theme and not a Theme + a React app, you can run theses few commands to refactor the template
|
||||
and remove unnecessary files.
|
||||
|
||||
```bash
|
||||
cd path/to/keycloakify-starter
|
||||
rm -r src/App
|
||||
mv src/keycloak-theme/* src/
|
||||
rm -r src/keycloak-theme
|
||||
|
||||
cat << EOF > src/main.tsx
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode, lazy, Suspense } from "react";
|
||||
import { kcContext as kcLoginThemeContext } from "./login/kcContext";
|
||||
import { kcContext as kcAccountThemeContext } from "./account/kcContext";
|
||||
|
||||
const KcLoginThemeApp = lazy(() => import("./login/KcApp"));
|
||||
const KcAccountThemeApp = lazy(() => import("./account/KcApp"));
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Suspense>
|
||||
{(()=>{
|
||||
|
||||
if( kcLoginThemeContext !== undefined ){
|
||||
return <KcLoginThemeApp kcContext={kcLoginThemeContext} />;
|
||||
}
|
||||
|
||||
if( kcAccountThemeContext !== undefined ){
|
||||
return <KcAccountThemeApp kcContext={kcAccountThemeContext} />;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"This app is a Keycloak theme" +
|
||||
"It isn't meant to be deployed outside of Keycloak"
|
||||
);
|
||||
|
||||
})()}
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
EOF
|
||||
|
||||
rm .dockerignore Dockerfile nginx.conf
|
||||
|
||||
cat << EOF > .github/workflows/ci.yaml
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx keycloakify
|
||||
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
outputs:
|
||||
from_version: \${{ steps.step1.outputs.from_version }}
|
||||
to_version: \${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: \${{ steps.step1.outputs.is_upgraded_version }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
branch: \${{ github.head_ref || github.ref }}
|
||||
|
||||
create_github_release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_if_version_upgraded
|
||||
# We create a release only if the version have been upgraded and we are on a default branch
|
||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' && github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx keycloakify
|
||||
- run: mv dist_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar
|
||||
- run: mv dist_keycloak/target/*.jar keycloak-theme.jar
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Release v\${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v\${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
target_commitish: \${{ github.head_ref || github.ref }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
files: |
|
||||
retrocompat-keycloak-theme.jar
|
||||
keycloak-theme.jar
|
||||
env:
|
||||
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
EOF
|
||||
```
|
||||
|
||||
You can also remove `oidc-spa`, `powerhooks`, `zod` and `tsafe` from your dependencies.
|
||||
|
|
84
index.html
84
index.html
|
@ -1,78 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!--
|
||||
Notice the use of %BASE_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%BASE_URL%favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
-->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%BASE_URL%favicon-32x32.png">
|
||||
|
||||
<title>Keycloakify starter</title>
|
||||
|
||||
<!-- NOTE: Here we import the WorkSans font as an example of how to import self hosted custom fonts. Don't keep it in your actual theme!
|
||||
SEE: https://docs.keycloakify.dev/limitations#self-hosted-fonts
|
||||
Don't forget to import your custom fonts in Storybook as well: https://github.com/keycloakify/keycloakify-starter/blob/bb019e66fb09166cb9af1e24e230994f59daa420/src/keycloak-theme/login/createPageStory.tsx#L21
|
||||
-->
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<style>
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
/*400*/
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
/*700*/
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta name="keycloakify-ignore-start">
|
||||
<script>console.log("This is logged Only in the main app, stripped out in the theme")</script>
|
||||
<meta name="keycloakify-ignore-end">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
35
nginx.conf
35
nginx.conf
|
@ -1,35 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Any route containing a file extension (e.g. /devicesfile.js)
|
||||
location ~ ^.+\..+$ {
|
||||
try_files $uri =404;
|
||||
|
||||
location ~* \.(?:html|json|txt)$ {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
# Vite generates filenames with hashes so we can
|
||||
# tell the browser to keep in cache the resources.
|
||||
location ~* \.(?:css|js|md|woff2?|eot|ttf|xml)$ {
|
||||
expires 1y;
|
||||
access_log off;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
96
package.json
96
package.json
|
@ -1,58 +1,42 @@
|
|||
{
|
||||
"name": "keycloakify-starter",
|
||||
"homepage": "https://starter.keycloakify.dev",
|
||||
"version": "6.1.10",
|
||||
"description": "A starter/demo project for keycloakify",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build-keycloak-theme": "yarn build && keycloakify",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"author": "u/garronej",
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"evt": "^2.5.7",
|
||||
"keycloakify": "^9.6.6",
|
||||
"oidc-spa": "^4.6.2",
|
||||
"powerhooks": "^1.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tsafe": "^1.6.6",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.0.2",
|
||||
"@storybook/addon-interactions": "^8.0.2",
|
||||
"@storybook/addon-links": "^8.0.2",
|
||||
"@storybook/addon-onboarding": "^8.0.2",
|
||||
"@storybook/blocks": "^8.0.2",
|
||||
"@storybook/react": "^8.0.2",
|
||||
"@storybook/react-vite": "^8.0.2",
|
||||
"@storybook/test": "^8.0.2",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"storybook": "^8.0.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-commonjs": "^0.10.1"
|
||||
},
|
||||
"_comment": "See https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092",
|
||||
"resolutions": {
|
||||
"jackspeak": "2.1.1"
|
||||
}
|
||||
"name": "keycloakify-starter",
|
||||
"version": "6.1.10",
|
||||
"description": "Starter for Keycloakify 10",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build-keycloak-theme": "npm run build && keycloakify build",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"format": "npx prettier . --write"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"keycloakify": "10.0.0-rc.72",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"storybook": "^8.1.6",
|
||||
"@storybook/react": "^8.1.6",
|
||||
"@storybook/react-vite": "^8.1.6",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prettier": "3.3.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
/*
|
||||
This file is only meant to be used by Storybook
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: normal; /*400*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("./worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("./worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: bold; /*700*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
|
@ -1,7 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,177 +1,49 @@
|
|||
# Terms of Service
|
||||
## Overview
|
||||
|
||||
## Presentation / Features
|
||||
This Terms of Service document outlines the rules and regulations for the use of **Example Company's** Services.
|
||||
|
||||
The SSP Cloud is a service (hereinafter referred to as "the service") implemented by the National Institute for Statistics and Economic Studies (hereinafter referred to as "Insee").
|
||||
## Acceptance of Terms
|
||||
|
||||
The SSP Cloud is an implementation of free software [Onyxia](https://github.com/InseeFrLab/onyxia) created and maintained by the innovation and technical instruction division of INSEE (information system management / innovation unit and information system strategy). The SSP Cloud is hosted by INSEE.
|
||||
By accessing and using our services, you acknowledge that you have read, understood, and agree to be bound by these terms. If you do not accept these terms, you are not authorized to use our services.
|
||||
|
||||
The SSP Cloud is a platform offering a "datalab" intended for _data science_ experiments on open data in which users can orchestrate services dedicated to the practice of _data science_ (development environments, databases, etc.). This service offering thus aims to familiarize users with new collaborative working methods using _open source_ statistical languages (R, python, Julia, etc.), _cloud computing_ type technologies, as well as to allow processing experiments. innovative statistics. The services offered are standard.
|
||||
## Description of Service
|
||||
|
||||
The SSP Cloud is aimed at officials of the official statistical system as well as teachers and students of the Group of National Schools of Economics and Statistics, allowing inter-service collaboration and cooperation with their ecosystem. Access can thus be granted on request and after decision of the governance bodies of the Cloud SSP to external collaborators and involved in the realization of experimental projects of the official statistical system. Projects involving non-open data are also subject to the decision of the governing bodies.
|
||||
**Example Service** (hereinafter referred to as "the Service") is a web-based solution offered by **Example Company** (hereinafter referred to as "the Company"). Our service provides users with access to [documentation](https://example.com/docs) and support for managing their projects effectively.
|
||||
|
||||
The SSP Cloud allows:
|
||||
## Modifications to the Terms of Service
|
||||
|
||||
- the orchestration of _data science_ trainings
|
||||
- access to _data science_ services
|
||||
- secure data storage
|
||||
- management of secrets, such as encryption keys
|
||||
- access to a code management service
|
||||
- orchestration of data processing flows
|
||||
The Company reserves the right to modify these terms at any time. Such modifications will be effective immediately upon posting the updated terms on our website. Your continued use of the Service after any such changes shall constitute your consent to such changes.
|
||||
|
||||
A user account is also used to connect to the service platform of [the Inter-ministerial Mutualization Free Software community](https://groupes.mim-libre.fr/).
|
||||
## Account Registration
|
||||
|
||||
## Legal Notice
|
||||
You may be required to register with the Service to access certain features. When registering, you agree to provide accurate, current, and complete information about yourself as requested.
|
||||
|
||||
Functional administration of the Cloud SSP: Insee
|
||||
## User Responsibilities
|
||||
|
||||
This site is published by the National Institute for Statistics and Economic Studies (Insee).
|
||||
INSEE
|
||||
88 avenue Verdier
|
||||
CS 70058
|
||||
92541 Montrouge cedex
|
||||
- **Data Security**: Users are responsible for safeguarding their login credentials and should not disclose their passwords to any third party.
|
||||
- **Acceptable Use**: Users are expected to use the Service in a responsible manner that does not infringe upon the rights of others.
|
||||
- **Content Ownership**: Users retain all rights to the content they upload to the Service but grant the Company a license to use and distribute this content as part of the Service.
|
||||
|
||||
Director of publication: Mr. Jean-Luc Tavernier
|
||||
## Intellectual Property
|
||||
|
||||
Administrator: Frédéric Comte
|
||||
All intellectual property rights related to the Service and its original content, features, and functionality are owned by the Company.
|
||||
|
||||
Maintenance of the _open source_ Onyxia project: Insee
|
||||
## Termination
|
||||
|
||||
Hosting: Insee - Innovation and technical instruction division
|
||||
The Company may terminate or suspend access to our Service immediately, without prior notice or liability, for any reason whatsoever, including, without limitation, breach of these Terms.
|
||||
|
||||
## Terms of use of the Service
|
||||
## Governing Law
|
||||
|
||||
The SSP Cloud datalab can be accessed from any browser connected to
|
||||
Internet. The use of a computer is recommended. Use of the datalab services is free.
|
||||
These Terms shall be governed and construed in accordance with the laws of [Your Country], without regard to its conflict of law provisions.
|
||||
|
||||
The user community is accessible on:
|
||||
## Contact Information
|
||||
|
||||
- Tchap, salon [SSP Cloud](https://www.tchap.gouv.fr/#/room/#SSPCloudXDpAw6v:agent.finances.tchap.gouv.fr)
|
||||
- Rocket Chat at MIM Libre, [SSP Cloud lounge](https://chat.mim-libre.fr/channel/sspcloud)
|
||||
For any questions about these Terms, please contact us at [support@example.com](mailto:support@example.com) or visit our [FAQ page](https://example.com/faq).
|
||||
|
||||
## Limits of use of the Service
|
||||
## Changes to Terms of Service
|
||||
|
||||
Public data and data can be processed on the datalab
|
||||
usual (working data without particular sensitivity). In the absence of specific authorization for a given experimental project, cannot be
|
||||
"protected" or "sensitive" data processed on the datalab, with or without a
|
||||
confidentiality intended to restrict distribution to a specific domain
|
||||
(statistical, commercial, industrial secrecy, etc.).
|
||||
We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect.
|
||||
|
||||
[EC: seems too "weak" to me, refer to the opinion of the UAJC on this point: if an agent puts sensitive data on the datalab, under his responsibility, what is the responsibility of his employer? from INSEE? can be added "after he has taken a legal opinion on the character 'protected' or 'sensitive' and that he informed his hierarchy ??]
|
||||
The "protected" or "sensitive" nature of the information stored or processed on the datalab
|
||||
is subject to the discretion of the user under his own
|
||||
responsibility.
|
||||
## Effective Date
|
||||
|
||||
## Roles, commitments and associated responsibilities
|
||||
|
||||
The service is made available by INSEE without other express guarantees or
|
||||
tacit than those provided herein. The service is based on benchmark open source technologies. However, it is not guaranteed that it
|
||||
is free from anomalies or errors. The service is therefore made available ** without
|
||||
guaranteed availability and performance **. As such, INSEE cannot
|
||||
be held responsible for loss and / or damage of any kind
|
||||
be, who couldbe caused as a result of a malfunction or
|
||||
unavailability of the service. Such situations will not give right to any
|
||||
financial compensation.
|
||||
|
||||
Each user has a personal storage space. By default, all the information deposited in a user's storage space is accessible only to him. Each user has the possibility of making public files stored in their personal storage space. Each user is responsible for making their files available to the public.
|
||||
|
||||
[EC: take the opinion of the UAJC, I do not know if it is the user specifically who is responsible for the processing or the institution on which he depends]
|
||||
Each user is responsible for processing all the experimental work he performs on the SSP Cloud.
|
||||
He must, if necessary, declare the personal processing carried out using the SSP Cloud to the data protection officer of his structure and inform the members thereof. [not sure that it is only the DPD of his structure who must be aware, also the DPD Insee?]
|
||||
[EC: in the case of a project involving several institutions, users must have previously established a data sharing / provision agreement.]
|
||||
|
||||
## Creating an account on the SSP Cloud
|
||||
|
||||
Access to the SSP Cloud requires prior registration and authentication.
|
||||
|
||||
## Experimental projects on sensitive data
|
||||
|
||||
** TODO **
|
||||
|
||||
Role of the project security manager
|
||||
|
||||
Enrollment of sensitive projects
|
||||
|
||||
Creation of collaborative spaces for sensitive projects
|
||||
|
||||
Creation and life cycle of spaces
|
||||
|
||||
## Processing of personal data
|
||||
|
||||
Data processing is based on the performance of the mission of providing a platform dedicated to experimentation and learning about data science for the benefit of the official statistical system.
|
||||
|
||||
The Service only collects the data strictly necessary for its implementation.
|
||||
artwork.
|
||||
|
||||
The processing of personal data within the meaning of Articles 9 and 10 of
|
||||
general data protection regulation (racial or ethnic origin,
|
||||
political opinions, religious or philosophical beliefs, belonging
|
||||
union, criminal convictions ...) is banned on the SSP Cloud.
|
||||
|
||||
[EC: same remark as above -> have the opinion of the Legal Unit]
|
||||
Personal data processed as part of an experiment carried out by a user, when there is any, is the responsibility of the entity
|
||||
administrative office from which the user originated. The
|
||||
arrangements for their treatment must be communicated by
|
||||
the user to the data protection officer of his entity
|
||||
administrative unit.
|
||||
|
||||
Regarding the scope of the SSP Cloud service, the purpose of processing
|
||||
concerns the management of the platform's accounts
|
||||
(creation / conservation / deletion), operation of the platform (monitoring,
|
||||
usage statistics) as well as the management of the services offered by the platform. Below is the list of
|
||||
transverse personal data whose processing is under the
|
||||
responsibility of INSEE.
|
||||
|
||||
** Suite to be managed with the DC POD **
|
||||
|
||||
> RL: @Fred, I put it a bit at random, I let you complete / amend
|
||||
|
||||
### Profile data
|
||||
|
||||
their first name, last name and email address (required);
|
||||
|
||||
freely:
|
||||
|
||||
- photo (see gitlab)
|
||||
- ...
|
||||
|
||||
### Trace data
|
||||
|
||||
They are collected each time a user connects and, for example,
|
||||
the use of a technical identifier, to trace connection operations and
|
||||
modification of the objects of the service database.
|
||||
|
||||
They are used for technical support purposes. They can also do
|
||||
subject to periodic review by the directors for control purposes and usage statistics.
|
||||
|
||||
### Cookie data
|
||||
|
||||
These cookies are only intended to allow the service to function and
|
||||
to facilitate its use by users according to the constraints of each typology.
|
||||
|
||||
- Session cookie: mandatory, it identifies the session of
|
||||
the user. The cookie is destroyed at the end of the session.
|
||||
|
||||
- Reauthentication cookie: optional, it allows you to re-authenticate
|
||||
the user logged in for the duration of the cookie (one year maximum)
|
||||
|
||||
## Modification and evolution of the Service
|
||||
|
||||
INSEE reserves the right to develop, modify or suspend,
|
||||
without notice, the Service for maintenance reasons or for any other
|
||||
reason deemed necessary. The information is then communicated to users via Tchap.
|
||||
The terms of these conditions of use may be modified or
|
||||
completed at any time, without notice, depending on changes
|
||||
made to the Service, changes in legislation or for any other reason
|
||||
deemed necessary. These modifications and updates are binding on the user who
|
||||
should therefore refer regularly to this section to verifythe
|
||||
general conditions in force (accessible from the home page).
|
||||
|
||||
## Contact
|
||||
|
||||
For technical problems and / or
|
||||
functionalities encountered on the platform, it is recommended, first of all
|
||||
time to solicit communities of peers in collaborative spaces
|
||||
provided for this purpose on Tchap and Rocket Chat-MIM Libre.
|
||||
|
||||
CNIL right of access for: innovation@insee.fr
|
||||
These terms are effective as of **[Insert Date]**.
|
||||
|
|
49
public/terms/es.md
Normal file
49
public/terms/es.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
## Resumen
|
||||
|
||||
Este documento de Términos de Servicio detalla las reglas y regulaciones para el uso de los servicios de **Empresa Ejemplo**.
|
||||
|
||||
## Aceptación de Términos
|
||||
|
||||
Al acceder y utilizar nuestros servicios, usted reconoce que ha leído, entendido y acepta estar vinculado por estos términos. Si no acepta estos términos, no está autorizado para usar nuestros servicios.
|
||||
|
||||
## Descripción del Servicio
|
||||
|
||||
**Servicio Ejemplo** (en adelante denominado "el Servicio") es una solución basada en la web ofrecida por **Empresa Ejemplo** (en adelante denominada "la Empresa"). Nuestro servicio proporciona a los usuarios acceso a [documentación](https://ejemplo.com/docs) y soporte para gestionar sus proyectos de manera efectiva.
|
||||
|
||||
## Modificaciones a los Términos de Servicio
|
||||
|
||||
La Empresa se reserva el derecho de modificar estos términos en cualquier momento. Dichas modificaciones entrarán en vigor inmediatamente después de la publicación de los términos actualizados en nuestro sitio web. Su uso continuado del Servicio después de tales cambios constituirá su consentimiento a dichos cambios.
|
||||
|
||||
## Registro de Cuenta
|
||||
|
||||
Puede ser necesario que se registre en el Servicio para acceder a ciertas características. Al registrarse, usted acepta proporcionar información precisa, actual y completa sobre sí mismo como se solicita.
|
||||
|
||||
## Responsabilidades del Usuario
|
||||
|
||||
- **Seguridad de Datos**: Los usuarios son responsables de salvaguardar sus credenciales de inicio de sesión y no deben divulgar sus contraseñas a terceros.
|
||||
- **Uso Aceptable**: Se espera que los usuarios utilicen el Servicio de manera responsable que no infrinja los derechos de otros.
|
||||
- **Propiedad del Contenido**: Los usuarios retienen todos los derechos sobre el contenido que cargan en el Servicio, pero otorgan a la Empresa una licencia para usar y distribuir este contenido como parte del Servicio.
|
||||
|
||||
## Propiedad Intelectual
|
||||
|
||||
Todos los derechos de propiedad intelectual relacionados con el Servicio y su contenido original, características y funcionalidad son propiedad de la Empresa.
|
||||
|
||||
## Terminación
|
||||
|
||||
La Empresa puede terminar o suspender su acceso a nuestro Servicio de inmediato, sin previo aviso ni responsabilidad, por cualquier motivo, incluido, entre otros, una violación de estos Términos.
|
||||
|
||||
## Ley Aplicable
|
||||
|
||||
Estos Términos se regirán e interpretarán de acuerdo con las leyes de [Su País], sin tener en cuenta sus disposiciones de conflicto de leyes.
|
||||
|
||||
## Información de Contacto
|
||||
|
||||
Para cualquier pregunta sobre estos Términos, contáctenos en [support@ejemplo.com](mailto:support@ejemplo.com) o visite nuestra [página de FAQ](https://ejemplo.com/faq).
|
||||
|
||||
## Cambios a los Términos de Servicio
|
||||
|
||||
Nos reservamos el derecho, a nuestra única discreción, de modificar o reemplazar estos Términos en cualquier momento. Si una revisión es material, proporcionaremos al menos 30 días de aviso antes de que los nuevos términos entren en vigor.
|
||||
|
||||
## Fecha de Efectividad
|
||||
|
||||
Estos términos son efectivos a partir del **[Insertar Fecha]**.
|
|
@ -1,180 +1,49 @@
|
|||
# Conditions générales d'utilisation
|
||||
## Vue d'ensemble
|
||||
|
||||
## Présentation / Fonctionnalités
|
||||
Ce document des Conditions Générales d'Utilisation détaille les règles et réglementations pour l'utilisation des services de **l'Entreprise Exemple**.
|
||||
|
||||
[EC: suite de la réunion d'aujourdhui : cela mériterait de différencier le SSP Cloud de l'instance d'Onyxia SSP Cloud]
|
||||
## Acceptation des Conditions
|
||||
|
||||
Le SSP Cloud est un service (ci après désigné par "le service") mis en œuvre par l'Institut national de la statistique et des études économiques (ci-après dénommé "l'Insee").
|
||||
En accédant et en utilisant nos services, vous reconnaissez avoir lu, compris et accepté d'être lié par ces conditions. Si vous n'acceptez pas ces termes, vous n'êtes pas autorisé à utiliser nos services.
|
||||
|
||||
Le SSP Cloud est une implémentation du logiciel libre [Onyxia](https://github.com/InseeFrLab/onyxia) créé et maintenu par la division innovation et instruction technique de l'Insee (direction du système d'information/unité innovation et stratégie du système d'information). L’hébergement du SSP Cloud est assuré par l'Insee.
|
||||
## Description du Service
|
||||
|
||||
[EC: j'enlèverai le "sur données ouvertes", puisque le SSP Cloud peut accueillir dans les donditions idoines des données sécurisées]
|
||||
Le SSP Cloud est une plateforme proposant un "datalab" destiné aux expérimentations de _data science_ sur données ouvertes dans lequel les utilisateurs peuvent orchestrer des services dédiés à la pratique de la _data science_ (environnements de développement, bases de données...). Cette offre de services vise ainsi à familiariser les utilisateurs avec de nouvelles méthodes de travail collaboratif mobilisant des langages statistiques _open source_ (R, python, Julia...), des technologies de type _cloud computing_ ainsi qu'à permettre d'expérimenter des traitements statistiques innovants. Les services proposés sont standards.
|
||||
**Service Exemple** (ci-après dénommé "le Service") est une solution basée sur le web offerte par **l'Entreprise Exemple** (ci-après dénommée "l'Entreprise"). Notre service offre aux utilisateurs un accès à la [documentation](https://exemple.com/docs) et un support pour gérer efficacement leurs projets.
|
||||
|
||||
Le SSP Cloud s’adresse aux agents du système statistique public ainsi qu'aux enseignants et étudiants du Groupe des écoles nationales d'économie et de statistique, permettant une collaboration interservices et la coopération avec leur écosystème. Des accès peuvent ainsi être accordés sur demande et après décision des organes de gouvernance du SSP Cloud à des collaborateurs extérieurs et impliqués dans la réalisation de projets expérimentaux du système statistique public. Les projets mobilisant des données non ouvertes sont aussi soumis à la décision des organes de gouvernance.
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
Le SSP Cloud permet :
|
||||
L'Entreprise se réserve le droit de modifier ces conditions à tout moment. De telles modifications entreront en vigueur immédiatement après la publication des termes mis à jour sur notre site web. Votre utilisation continue du Service après de tels changements constitue votre consentement à ces modifications.
|
||||
|
||||
- l'orchestration de formations de _data science_
|
||||
- l'accès à des services de _data science_
|
||||
- le stockage sécurisé de données
|
||||
- la gestion de secrets, tels que des clés de chiffrement
|
||||
- l'accès à un service de gestion de code
|
||||
- l'orchestration de flux de traitement de données
|
||||
## Inscription au Compte
|
||||
|
||||
Un compte utilisateur permet également de se connecter à la plateforme de services de la communauté Mutualisation Inter-ministérielle Logiciels Libres (<https://groupes.mim-libre.fr/>).
|
||||
Vous devrez peut-être vous inscrire au Service pour accéder à certaines fonctionnalités. Lors de l'inscription, vous acceptez de fournir des informations précises, actuelles et complètes vous concernant, comme demandé.
|
||||
|
||||
## Mentions légales
|
||||
## Responsabilités des Utilisateurs
|
||||
|
||||
Administration fonctionnelle du SSP Cloud : Insee
|
||||
- **Sécurité des Données** : Les utilisateurs sont responsables de la sauvegarde de leurs identifiants de connexion et ne doivent divulguer leurs mots de passe à aucun tiers.
|
||||
- **Utilisation Acceptable** : Les utilisateurs sont censés utiliser le Service de manière responsable qui ne porte pas atteinte aux droits d'autrui.
|
||||
- **Propriété du Contenu** : Les utilisateurs conservent tous les droits sur le contenu qu'ils téléchargent sur le Service mais accordent à l'Entreprise une licence pour utiliser et distribuer ce contenu dans le cadre du Service.
|
||||
|
||||
Ce site est édité par l'Institut national de la statistique et des études économiques (Insee).
|
||||
Insee
|
||||
88 avenue Verdier
|
||||
CS 70058
|
||||
92541 Montrouge cedex
|
||||
## Propriété Intellectuelle
|
||||
|
||||
Directeur de la publication : Monsieur Jean-Luc Tavernier
|
||||
Tous les droits de propriété intellectuelle relatifs au Service et à son contenu original, fonctionnalités et fonctionnement sont détenus par l'Entreprise.
|
||||
|
||||
Administrateur : Frédéric Comte
|
||||
## Résiliation
|
||||
|
||||
Maintenance du projet _open source_ Onyxia : Insee
|
||||
L'Entreprise peut résilier ou suspendre votre accès à notre Service immédiatement, sans préavis ni responsabilité, pour quelque raison que ce soit, y compris, sans limitation, en cas de violation de ces Conditions.
|
||||
|
||||
Hébergement : Insee - Division innovation et instruction technique
|
||||
## Loi Applicable
|
||||
|
||||
## Modalités d’utilisation du Service
|
||||
Ces Conditions seront régies et interprétées conformément aux lois de [Votre Pays], sans égard à ses dispositions de conflit de lois.
|
||||
|
||||
Le datalab SSP Cloud est accessible depuis n’importe quel navigateur connecté à
|
||||
Internet. L'utilisation d'un ordinateur est recommandée. L’utilisation des services du datalab est gratuite.
|
||||
## Informations de Contact
|
||||
|
||||
La communauté d'utilisateurs est accessible sur :
|
||||
Pour toute question concernant ces Conditions, veuillez nous contacter à [support@exemple.com](mailto:support@exemple.com) ou visitez notre [page FAQ](https://exemple.com/faq).
|
||||
|
||||
- Tchap, salon [SSP Cloud](https://www.tchap.gouv.fr/#/room/#SSPCloudXDpAw6v:agent.finances.tchap.gouv.fr)
|
||||
- Rocket Chat du MIM Libre, salon [SSP Cloud](https://chat.mim-libre.fr/channel/sspcloud)
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
## Limites d’utilisation du Service
|
||||
Nous nous réservons le droit, à notre seule discrétion, de modifier ou de remplacer ces Conditions à tout moment. Si une révision est importante, nous vous fournirons un préavis d'au moins 30 jours avant que les nouveaux termes prennent effet.
|
||||
|
||||
Peuvent être traitées sur le datalab les données publiques et données
|
||||
usuelles (données de travail sans sensibilité particulière). En l'absence d'autorisation spécifique pour un projet d'expérimentation donné, ne peuvent être
|
||||
traitées sur le datalab les données ‘protégées’ ou ‘sensibles’, avec ou sans marque de
|
||||
confidentialité destinée à restreindre la diffusion à un domaine spécifique
|
||||
(secret statistique, commercial, industriel..).
|
||||
## Date d'Effet
|
||||
|
||||
[EC: me semble trop "faible", se référer à l'avis de l'UAJC sur ce point : si un agent met des données sensibles sur le datalab, sous sa responsabilité, quelle est la responsabilité de son employeur? de l'Insee ? peut être ajouter "après qu'il ait pris un avis juridique sur le caractère 'protégé' ou 'sensible' et qu'il en ait informé sa hiérarchie??]
|
||||
Le caractère ‘protégé’ ou ‘sensible’ des informations stockées ou traitées sur le datalab
|
||||
est soumis à l’appréciation de l’utilisateur sous sa propre
|
||||
responsabilité.
|
||||
|
||||
## Les rôles, engagements et responsabilités associées
|
||||
|
||||
Le service est mis à disposition par l'Insee sans autres garanties expresses ou
|
||||
tacites que celles qui sont prévues par les présentes. Le service s’appuie sur des technologies open source de référence. Toutefois, il n’est pas garanti qu’il
|
||||
soit exempt d’anomalies ou erreurs. Le service est donc mis à disposition **sans
|
||||
garantie sur sa disponibilité et ses performances**. A ce titre, l'Insee ne peut
|
||||
être tenue responsable des pertes et/ou préjudices, de quelque nature qu’ils
|
||||
soient, qui pourraient être causés à la suite d’un dysfonctionnement ou une
|
||||
indisponibilité du service. De telles situations n'ouvriront droit à aucune
|
||||
compensation financière.
|
||||
|
||||
Chaque utilisateur dispose d'un espace de stockage personnel. Par défaut, toutes les informations déposées dans un espace de stockage d'un utilisateur ne sont accessibles qu'à lui seul. Chaque utilisateur a la possibilité de rendre publics des fichiers stockés dans son espace de stockage personnel. Chaque utilisateur est responsable de la mise à disposition publique de ses fichiers.
|
||||
|
||||
[EC : prendre l'avis de l'UAJC, je ne sais pas si c'est l'utilisateur nommément qui est responsable du traitement ou bien l'institution dont il dépend]
|
||||
Chaque utilisateur est responsable de traitement pour l’ensemble des travaux d'expérimentation qu'il réalise sur le SSP Cloud.
|
||||
Il doit, le cas échant, déclarer les traitements à caractère personnel réalisés à l'aide du SSP Cloud au délégué à la protection des données de sa structure et en informer les membres. [pas sur que ce soit uniquement le DPD de sa structure qui doit être au courant, aussi le DPD Insee?]
|
||||
[EC : dans le cas d'un projet faisant intervenir plusieurs institutions, les utilisateurs doivent avoir au préalable établi un conventionnement de partage/ mise à disposition des données.]
|
||||
|
||||
## La création de compte sur le SSP Cloud
|
||||
|
||||
L'accès au SSP Cloud nécessite une inscription préalable et une authentification.
|
||||
|
||||
## Les projets d'expérimentation sur données sensibles
|
||||
|
||||
**TODO**
|
||||
|
||||
Rôle du responsable de sécurité du projet
|
||||
|
||||
Enrôlement des projets sensibles
|
||||
|
||||
Création d'espaces collaboratifs pour les projets sensibles
|
||||
|
||||
Création et cycle de vie des espaces
|
||||
|
||||
## Traitement des données à caractère personnel
|
||||
|
||||
Le traitement des données se fonde sur l’exécution de la mission que constitue la mise à disposition d'une plateforme dédiée à l'expérimentation et à l'apprentissage de la datascience au bénéfice du système statistique public.
|
||||
|
||||
Le Service ne collecte que les données strictement nécessaires à sa mise en
|
||||
œuvre.
|
||||
|
||||
Le traitement de données à caractère personnel au sens des articles 9 et 10 du
|
||||
règlement général sur la protection des données (origine raciale ou ethnique,
|
||||
opinions politiques, convictions religieuses ou philosophiques, appartenance
|
||||
syndicale, condamnations pénales...) est proscrit sur le SSP Cloud.
|
||||
|
||||
[EC: meme remarque que ci-dessus --> avoir l'avis de l'Unité juridique]
|
||||
Les données à caractère personnel traitées dans le cadre d'une expérimentation réalisée par un utilisateur, quand il y en a, relèvent de la responsabilité de l’entité
|
||||
administrative dont est issu l’utilisateur. Les
|
||||
dispositions relatives à leur traitement doivent être communiquées par
|
||||
l'utilisateur au délégué à la protection des données de son entité
|
||||
administrative de rattachement.
|
||||
|
||||
Pour ce qui est du périmètre du service SSP Cloud, la finalité de traitement
|
||||
concerne la gestion des comptes de la plateforme
|
||||
(création/conservation/suppression), l’exploitation de la plateforme (suivi,
|
||||
statistiques d’usages) ainsi que la gestion des services offerts par la plateforme. Ci-dessous la liste des
|
||||
données à caractère personnel transverses dont le traitement est sous la
|
||||
responsabilité de l'Insee.
|
||||
|
||||
**Suite à gérer avec le DC POD**
|
||||
|
||||
> RL : @Fred, je mets un peu au hasard, je te laisse compléter/amender
|
||||
|
||||
### Données relatives au profil
|
||||
|
||||
ses prénom, nom et adresse mail (obligatoire) ;
|
||||
|
||||
de façon libre :
|
||||
|
||||
- photo (cf. gitlab)
|
||||
- ...
|
||||
|
||||
### Données de trace
|
||||
|
||||
Elles sont collectées à chaque connexion d'un utilisateur et permettent, par
|
||||
l’utilisation d’un identifiant technique, de tracer les opérations de connexion et
|
||||
de modification des objets de la base de données du service.
|
||||
|
||||
Elles servent à des fins de support technique. Elles peuvent également faire
|
||||
l'objet d'une revue périodique de la part des administrateurs à des fins de contrôle et de statistiques d'usage.
|
||||
|
||||
### Les données de cookies
|
||||
|
||||
Ces cookies n’ont pour objet que de permettre le fonctionnement du service et
|
||||
de faciliter son usage par les utilisateurs selon les contraintes chaque typologie.
|
||||
|
||||
- Cookie de session : obligatoire , il permet d'identifier la session de
|
||||
l'utilisateur. Le cookie est détruit à la fin de la session.
|
||||
|
||||
- Cookie de réauthentification : optionnel, il permet de ré-authentifier
|
||||
l'utilisateur connecté pendant la durée du cookie (un an maximum)
|
||||
|
||||
## Modification et évolution du Service
|
||||
|
||||
L'Insee se réserve la liberté de faire évoluer, de modifier ou de suspendre,
|
||||
sans préavis, le Service pour des raisons de maintenance ou pour tout autre
|
||||
motif jugé nécessaire. L'information est alors communiquée aux utilisateurs via Tchap.
|
||||
Les termes des présentes conditions d’utilisation peuvent être modifiés ou
|
||||
complétés à tout moment, sans préavis, en fonction des modifications
|
||||
apportées au Service, de l’évolution de la législation ou pour tout autre motif
|
||||
jugé nécessaire. Ces modifications et mises à jour s’imposent à l’utilisateur qui
|
||||
doit, en conséquence, se référer régulièrement à cette rubrique pour vérifier les
|
||||
conditions générales en vigueur (accessible depuis la page d’accueil).
|
||||
|
||||
## Contact
|
||||
|
||||
Pour les problèmes techniques et/ou
|
||||
fonctionnels rencontrés sur la plateforme, il est conseillé, dans un premier
|
||||
temps de solliciter les communautés de pairs dans les espaces collaboratifs
|
||||
prévus à cet effet sur Tchap et Rocket Chat-MIM Libre.
|
||||
|
||||
Droit d’accès CNIL pour : <innovation@insee.fr>
|
||||
Ces conditions sont effectives à partir du **[Insérer la Date]**.
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
|
||||
.App {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.App-payload {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
color: white;
|
||||
/* link color */
|
||||
a {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.App-logo-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 15vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo.rotate {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import "./App.css";
|
||||
import reactSvgUrl from "./assets/react.svg";
|
||||
import viteSvgUrl from "./assets/vite.svg";
|
||||
import { OidcProvider, useOidc, getKeycloakAccountUrl } from "./oidc";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
// To integrate Keycloak to your React App you have many options such as:
|
||||
// - https://www.npmjs.com/package/keycloak-js
|
||||
// - https://github.com/authts/oidc-client-ts
|
||||
// - https://github.com/authts/react-oidc-context
|
||||
// In this starter we use oidc-spa instead
|
||||
// It's a new library made by us, the Keycloakify team.
|
||||
// Check it out: https://github.com/keycloakify/oidc-spa
|
||||
<OidcProvider>
|
||||
<ContextualizedApp />
|
||||
</OidcProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextualizedApp() {
|
||||
|
||||
const { isUserLoggedIn, login, logout, oidcTokens } = useOidc();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div>
|
||||
<div className="App-payload">
|
||||
{isUserLoggedIn ?
|
||||
(
|
||||
<>
|
||||
|
||||
<h1>Hello {oidcTokens.decodedIdToken.name} !</h1>
|
||||
<a
|
||||
href={getKeycloakAccountUrl({ locale: "en" })}
|
||||
>
|
||||
Link to your Keycloak account
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => logout({ redirectTo: "home" })}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<Jwt />
|
||||
</>
|
||||
)
|
||||
:
|
||||
(
|
||||
<button
|
||||
onClick={() => login({
|
||||
doesCurrentHrefRequiresAuth: false,
|
||||
//extraQueryParams: { kc_idp_hint: "google" }
|
||||
})}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="App-logo-wrapper">
|
||||
<img src={reactSvgUrl} className="App-logo rotate" alt="logo" />
|
||||
|
||||
<img src={viteSvgUrl} className="App-logo" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function Jwt() {
|
||||
|
||||
const { oidcTokens } = useOidc({
|
||||
assertUserLoggedIn: true
|
||||
});
|
||||
|
||||
// NOTE: Use `Bearer ${oidcTokens.accessToken}` as the Authorization header to call your backend
|
||||
// Here we just display the decoded id token
|
||||
|
||||
return (
|
||||
<pre style={{ textAlign: "left" }}>
|
||||
{JSON.stringify(oidcTokens.decodedIdToken, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,4 +0,0 @@
|
|||
import App from "./App";
|
||||
export * from "./App";
|
||||
|
||||
export default App;
|
|
@ -1,60 +0,0 @@
|
|||
// See documentation of oidc-spa for more details:
|
||||
// https://docs.oidc-spa.dev
|
||||
|
||||
import { createReactOidc } from "oidc-spa/react";
|
||||
import { z } from "zod";
|
||||
|
||||
//On older Keycloak version you need the /auth (e.g: http://localhost:8080/auth)
|
||||
//On newer version you must remove it (e.g: http://localhost:8080 )
|
||||
const keycloakUrl = "https://cloud-iam.keycloakify.dev/";
|
||||
const keycloakRealm = "keycloakify";
|
||||
const keycloakClientId= "starter";
|
||||
|
||||
export const { OidcProvider, useOidc } = createReactOidc({
|
||||
issuerUri: `${keycloakUrl}/realms/${keycloakRealm}`,
|
||||
clientId: keycloakClientId,
|
||||
// NOTE: You can also pass queries params when calling login()
|
||||
extraQueryParams: () => ({
|
||||
// This adding ui_locales to the url will ensure the consistency of the language between the app and the login pages
|
||||
// If your app implements a i18n system (like i18nifty.dev for example) you should use this and replace "en" by the
|
||||
// current language of the app.
|
||||
// On the other side you will find kcContext.locale.currentLanguageTag to be whatever you set here.
|
||||
"ui_locales": "en",
|
||||
"my_custom_param": "value of foo transferred to login page"
|
||||
}),
|
||||
publicUrl: import.meta.env.BASE_URL,
|
||||
decodedIdTokenSchema: z.object({
|
||||
// Use https://jwt.io/ to tell what's in your idToken
|
||||
// It will depend of your Keycloak configuration.
|
||||
// Here I declare only two field on the type but actually there are
|
||||
// Many more things available.
|
||||
sub: z.string(),
|
||||
name: z.string(),
|
||||
preferred_username: z.string(),
|
||||
// This is a custom attribute set up in our Keycloak configuration
|
||||
// it's not present by default.
|
||||
// See https://docs.keycloakify.dev/realtime-input-validation#getting-your-custom-user-attribute-to-be-included-in-the-jwt
|
||||
favorite_pet: z.union([z.literal("cat"), z.literal("dog"), z.literal("bird")])
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export function getKeycloakAccountUrl(
|
||||
params: {
|
||||
locale: string;
|
||||
}
|
||||
){
|
||||
const { locale } = params;
|
||||
|
||||
const accountUrl = new URL(`${keycloakUrl}/realms/${keycloakRealm}/account`);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append("kc_locale", locale);
|
||||
searchParams.append("referrer", keycloakClientId);
|
||||
searchParams.append("referrer_uri", window.location.href);
|
||||
|
||||
accountUrl.search = searchParams.toString();
|
||||
|
||||
return accountUrl.toString();
|
||||
}
|
12
src/account/KcContext.ts
Normal file
12
src/account/KcContext.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/account";
|
||||
import type { KcEnvName, ThemeName } from "../kc.gen";
|
||||
|
||||
export type KcContextExtension = {
|
||||
themeName: ThemeName;
|
||||
properties: Record<KcEnvName, string> & {};
|
||||
};
|
||||
|
||||
export type KcContextExtensionPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
33
src/account/KcPage.tsx
Normal file
33
src/account/KcPage.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Suspense } from "react";
|
||||
import type { ClassKey } from "keycloakify/account";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/account/DefaultPage";
|
||||
import Template from "keycloakify/account/Template";
|
||||
|
||||
export default function KcPage(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
const { i18n } = useI18n({ kcContext });
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
default:
|
||||
return (
|
||||
<DefaultPage
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
classes={classes}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = {} satisfies { [key in ClassKey]?: string };
|
42
src/account/KcPageStory.tsx
Normal file
42
src/account/KcPageStory.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { createGetKcContextMock } from "keycloakify/account/KcContext";
|
||||
import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext";
|
||||
import KcPage from "./KcPage";
|
||||
import { themeNames, kcEnvDefaults } from "../kc.gen";
|
||||
|
||||
const kcContextExtension: KcContextExtension = {
|
||||
themeName: themeNames[0],
|
||||
properties: {
|
||||
...kcEnvDefaults
|
||||
}
|
||||
};
|
||||
const kcContextExtensionPerPage: KcContextExtensionPerPage = {};
|
||||
|
||||
export const { getKcContextMock } = createGetKcContextMock({
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage,
|
||||
overrides: {},
|
||||
overridesPerPage: {}
|
||||
});
|
||||
|
||||
export function createKcPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
const { pageId } = params;
|
||||
|
||||
function KcPageStory(props: {
|
||||
kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||
}) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return <KcPage kcContext={kcContextMock} />;
|
||||
}
|
||||
|
||||
return { KcPageStory };
|
||||
}
|
5
src/account/i18n.ts
Normal file
5
src/account/i18n.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createUseI18n } from "keycloakify/account";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
21
src/kc.gen.ts
Normal file
21
src/kc.gen.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* prettier-ignore-start */
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file is auto-generated by Keycloakify
|
||||
|
||||
export type ThemeName = "keycloakify-starter";
|
||||
|
||||
export const themeNames: ThemeName[] = ["keycloakify-starter"];
|
||||
|
||||
export type KcEnvName = never;
|
||||
|
||||
export const kcEnvNames: KcEnvName[] = [];
|
||||
|
||||
export const kcEnvDefaults: Record<KcEnvName, string> = {};
|
||||
|
||||
/* prettier-ignore-end */
|
|
@ -1,29 +0,0 @@
|
|||
Your theme source files should be located in a keycloak-theme directory somewhere in your src directory OR at the root of your directory.
|
||||
Acceptable directory strucuture:
|
||||
|
||||
```txt
|
||||
src/
|
||||
keycloak-theme/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
|
||||
===OR===
|
||||
|
||||
src/
|
||||
foo/
|
||||
bar/
|
||||
keycloak-theme/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
|
||||
===OR===
|
||||
|
||||
src/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
```
|
||||
|
||||
You don't need to have all three variant of the theme. If you only need the login theme for example you can have only the login directory.
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
If you use global CSS like we do here(not recommended)
|
||||
Be mindful that the CSS of the login theme may clash with the CSS of the account theme in Storybook (and only in storybook).
|
||||
This is why I made sure to use .my-root-account-class instead of .my-root-class that is already used in the login theme.
|
||||
*/
|
||||
|
||||
.my-root-account-class {
|
||||
background: url(./assets/background.svg) no-repeat center center fixed;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { PageProps } from "keycloakify/account";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import Template from "./Template";
|
||||
|
||||
const Password = lazy(() => import("./pages/Password"));
|
||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||
const Fallback = lazy(()=> import("keycloakify/account"));
|
||||
|
||||
const classes = {
|
||||
"kcBodyClass": "my-root-account-class"
|
||||
} satisfies PageProps["classes"];
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext; }) {
|
||||
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "password.ftl": return <Password {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
default: return <Fallback {...{ kcContext, i18n, classes }} Template={Template} doUseDefaultCss={true} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
|
||||
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
const { isReady } = usePrepareTemplate({
|
||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||
"styles": [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("accountManagementTitle")
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="navbar navbar-default navbar-pf navbar-main header">
|
||||
<nav className="navbar" role="navigation">
|
||||
<div className="navbar-header">
|
||||
<div className="container">
|
||||
<h1 className="navbar-title">Keycloak</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-collapse navbar-collapse-1">
|
||||
<div className="container">
|
||||
<ul className="nav navbar-nav navbar-utility">
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<li>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{referrer?.url && (
|
||||
<li>
|
||||
<a href={referrer.url} id="referrer">
|
||||
{msg("backTo", referrer.name)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<a href={url.getLogoutUrl()}>{msg("doSignOut")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="container">
|
||||
<div className="bs-sidebar col-sm-3">
|
||||
<ul>
|
||||
<li className={clsx(active === "account" && "active")}>
|
||||
<a href={url.accountUrl}>{msg("account")}</a>
|
||||
</li>
|
||||
{features.passwordUpdateSupported && (
|
||||
<li className={clsx(active === "password" && "active")}>
|
||||
<a href={url.passwordUrl}>{msg("password")}</a>
|
||||
</li>
|
||||
)}
|
||||
<li className={clsx(active === "totp" && "active")}>
|
||||
<a href={url.totpUrl}>{msg("authenticator")}</a>
|
||||
</li>
|
||||
{features.identityFederation && (
|
||||
<li className={clsx(active === "social" && "active")}>
|
||||
<a href={url.socialUrl}>{msg("federatedIdentity")}</a>
|
||||
</li>
|
||||
)}
|
||||
<li className={clsx(active === "sessions" && "active")}>
|
||||
<a href={url.sessionsUrl}>{msg("sessions")}</a>
|
||||
</li>
|
||||
<li className={clsx(active === "applications" && "active")}>
|
||||
<a href={url.applicationsUrl}>{msg("applications")}</a>
|
||||
</li>
|
||||
{features.log && (
|
||||
<li className={clsx(active === "log" && "active")}>
|
||||
<a href={url.logUrl}>{msg("log")}</a>
|
||||
</li>
|
||||
)}
|
||||
{realm.userManagedAccessAllowed && features.authorization && (
|
||||
<li className={clsx(active === "authorization" && "active")}>
|
||||
<a href={url.resourceUrl}>{msg("myResources")}</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-9 content-area">
|
||||
{message !== undefined && (
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className="pficon pficon-ok"></span>}
|
||||
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
|
||||
<span className="kc-feedback-text">{message.summary}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<svg width="1521" height="961" viewBox="0 0 1521 961" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.4">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path d="M289.342 250.792L427.47 389.611C471.444 433.805 542.707 433.805 586.621 389.611L724.749 250.792L507.046 32L289.342 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M586.267 389.258L586.267 389.258C542.548 433.256 471.603 433.256 427.824 389.258L290.047 250.792L507.046 32.7089L724.044 250.792L586.267 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dd)">
|
||||
<path d="M32 509.755L170.128 648.573C214.103 692.767 285.365 692.767 329.28 648.573L467.408 509.755L249.704 290.962L32 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M328.925 648.221L328.925 648.221C285.206 692.218 214.262 692.219 170.483 648.221L32.7054 509.755L249.704 291.671L466.702 509.755L328.925 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dd)">
|
||||
<path d="M289.281 767.036L427.409 905.854C471.384 950.048 542.646 950.048 586.561 905.854L724.689 767.036L506.985 548.243L289.281 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M586.206 905.502L586.206 905.502C542.487 949.499 471.543 949.5 427.764 905.502L289.986 767.036L506.985 548.952L723.983 767.036L586.206 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dd)">
|
||||
<path d="M546.562 509.755L684.69 648.573C728.665 692.767 799.927 692.767 843.842 648.573L981.97 509.755L764.266 290.962L546.562 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M843.487 648.221L843.487 648.221C799.768 692.218 728.824 692.219 685.044 648.221L547.267 509.755L764.266 291.671L981.264 509.755L843.487 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dd)">
|
||||
<path d="M803.843 250.792L941.971 389.611C985.945 433.805 1057.21 433.805 1101.12 389.611L1239.25 250.792L1021.55 32L803.843 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M1100.77 389.258L1100.77 389.258C1057.05 433.256 986.105 433.256 942.325 389.258L804.548 250.792L1021.55 32.7089L1238.55 250.792L1100.77 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_dd)">
|
||||
<path d="M1062.81 509.755L1200.93 648.573C1244.91 692.767 1316.17 692.767 1360.08 648.573L1498.21 509.755L1280.51 290.962L1062.81 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M1359.73 648.221L1359.73 648.221C1316.01 692.218 1245.07 692.219 1201.29 648.221L1063.51 509.755L1280.51 291.671L1497.51 509.755L1359.73 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_dd)">
|
||||
<path d="M805.524 767.036L943.653 905.854C987.627 950.048 1058.89 950.048 1102.8 905.854L1240.93 767.036L1023.23 548.243L805.524 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M1102.45 905.502L1102.45 905.502C1058.73 949.499 987.786 949.5 944.007 905.502L806.23 767.036L1023.23 548.952L1240.23 767.036L1102.45 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="257.342" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd" x="0" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd" x="257.281" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_dd" x="514.562" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter4_dd" x="771.843" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter5_dd" x="1030.81" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter6_dd" x="773.524" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 9.3 KiB |
|
@ -1,30 +0,0 @@
|
|||
import { getKcContext, type KcContext } from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>; }) {
|
||||
|
||||
const { kcContext } = getKcContext({
|
||||
mockPageId: pageId,
|
||||
storyPartialKcContext: params.kcContext
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
|
||||
<link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
|
||||
<KcApp kcContext={kcContext} />
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { createUseI18n } from "keycloakify/account";
|
||||
|
||||
//NOTE: See src/login/i18n.ts for instructions on customization of i18n messages.
|
||||
export const { useI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = NonNullable<ReturnType<typeof useI18n>>;
|
|
@ -1,23 +0,0 @@
|
|||
import { createGetKcContext } from "keycloakify/account";
|
||||
|
||||
export type KcContextExtension =
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
|
||||
|
||||
export const { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||
mockData: [
|
||||
{
|
||||
pageId: "my-extra-page-2.ftl",
|
||||
someCustomValue: "foo bar"
|
||||
}
|
||||
],
|
||||
mockProperties: {
|
||||
MY_ENV_VARIABLE: "Mocked value"
|
||||
}
|
||||
});
|
||||
|
||||
export const { kcContext } = getKcContext({
|
||||
//mockPageId: "password.ftl",
|
||||
});
|
||||
|
||||
export type KcContext = NonNullable<ReturnType<typeof getKcContext>["kcContext"]>;
|
|
@ -1,15 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="my-extra-page-1" >
|
||||
<h1>Hello world 1</h1>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
// someCustomValue is declared by you in ../kcContext.ts
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="my-extra-page-2" >
|
||||
<h1>Hello world 2</h1>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "password.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "account/Password",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory
|
||||
kcContext={{
|
||||
message: { type: "success", summary: "This is a test message" }
|
||||
}}
|
||||
/>
|
||||
};
|
|
@ -1,105 +0,0 @@
|
|||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||
}
|
||||
});
|
||||
|
||||
const { url, password, account, stateChecker } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("changePasswordHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div className="col-md-2 subtitle">
|
||||
<span className="subtitle">{msg("allFieldsRequired")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={url.passwordUrl} className="form-horizontal" method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={account.username ?? ""}
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
style={{ "display": "none" }}
|
||||
/>
|
||||
|
||||
{password.passwordSet && (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password" className="control-label">
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-new" className="control-label">
|
||||
{msg("passwordNew")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-confirm" className="control-label two-lines">
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
{msg("doSave")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
.my-color {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.my-font {
|
||||
font-family: 'Work Sans';
|
||||
}
|
||||
|
||||
.my-root-class {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.my-root-class body {
|
||||
background: url(./assets/background.svg) no-repeat center center fixed;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import Fallback, { type PageProps } from "keycloakify/login";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import Template from "./Template";
|
||||
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
|
||||
const Register = lazy(() => import("./pages/Register"));
|
||||
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
|
||||
const Terms = lazy(() => import("./pages/Terms"));
|
||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||
const Info = lazy(() => import("keycloakify/login/pages/Info"));
|
||||
|
||||
// This is like adding classes to theme.properties
|
||||
// https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties
|
||||
const classes = {
|
||||
// NOTE: The classes are defined in ./KcApp.css
|
||||
"kcHtmlClass": "my-root-class",
|
||||
"kcHeaderWrapperClass": "my-color my-font"
|
||||
} satisfies PageProps["classes"];
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext; }) {
|
||||
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
if (i18n === null) {
|
||||
//NOTE: Text resources for the current language are still being downloaded, we can't display anything yet.
|
||||
//We could display a loading progress but it's usually a matter of milliseconds.
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Examples assuming i18n.currentLanguageTag === "en":
|
||||
* i18n.msg("access-denied") === <span>Access denied</span>
|
||||
* i18n.msg("foo") === <span>foo in English</span>
|
||||
*/
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl": return <Login {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "register.ftl": return <Register {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />
|
||||
case "terms.ftl": return <Terms {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
// Removes those pages in you project. They are included to show you how to implement keycloak pages
|
||||
// that are not yes implemented by Keycloakify.
|
||||
// See: https://docs.keycloakify.dev/limitations#some-pages-still-have-the-default-theme.-why
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
// We choose to use the default Template for the Info page and to download the theme resources.
|
||||
// This is just an example to show you what is possible. You likely don't want to keep this as is.
|
||||
case "info.ftl": return (
|
||||
<Info
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={lazy(() => import("keycloakify/login/Template"))}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
default: return <Fallback {...{ kcContext, i18n, classes }} Template={Template} doUseDefaultCss={true} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||
import { type TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import keycloakifyLogoPngUrl from "./assets/keycloakify-logo.png";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
displayWide = false,
|
||||
showAnotherWayIfPresent = true,
|
||||
headerNode,
|
||||
showUsernameNode = null,
|
||||
infoNode = null,
|
||||
kcContext,
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
classes,
|
||||
children
|
||||
} = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const { isReady } = usePrepareTemplate({
|
||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||
"styles": [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": getClassName("kcBodyClass"),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("loginTitle", kcContext.realm.displayName)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`Value of MY_ENV_VARIABLE on the Keycloak server: "${kcContext.properties.MY_ENV_VARIABLE}"`);
|
||||
}, []);
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLoginClass")}>
|
||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
||||
<div
|
||||
id="kc-header-wrapper"
|
||||
className={getClassName("kcHeaderWrapperClass")}
|
||||
style={{ "fontFamily": '"Work Sans"' }}
|
||||
>
|
||||
{/*
|
||||
Here we are referencing the `keycloakify-logo.png` in the `public` directory.
|
||||
When possible don't use this approach, instead ...
|
||||
*/}
|
||||
<img src={`${import.meta.env.BASE_URL}keycloakify-logo.png`} alt="Keycloakify logo" width={50} />
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}!!!
|
||||
{/* ...rely on the bundler to import your assets, it's more efficient */}
|
||||
<img src={keycloakifyLogoPngUrl} alt="Keycloakify logo" width={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
|
||||
<header className={getClassName("kcFormHeaderClass")}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<div id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
|
||||
{message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
|
||||
{message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
|
||||
{message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
"__html": message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && showAnotherWayIfPresent && (
|
||||
<form
|
||||
id="kc-select-try-another-way-form"
|
||||
action={url.loginAction}
|
||||
method="post"
|
||||
className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<a
|
||||
href="#"
|
||||
id="try-another-way"
|
||||
onClick={() => {
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{msg("doTryAnotherWay")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={getClassName("kcSignUpClass")}>
|
||||
<div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<svg width="1521" height="961" viewBox="0 0 1521 961" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.4">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path d="M289.342 250.792L427.47 389.611C471.444 433.805 542.707 433.805 586.621 389.611L724.749 250.792L507.046 32L289.342 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M586.267 389.258L586.267 389.258C542.548 433.256 471.603 433.256 427.824 389.258L290.047 250.792L507.046 32.7089L724.044 250.792L586.267 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dd)">
|
||||
<path d="M32 509.755L170.128 648.573C214.103 692.767 285.365 692.767 329.28 648.573L467.408 509.755L249.704 290.962L32 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M328.925 648.221L328.925 648.221C285.206 692.218 214.262 692.219 170.483 648.221L32.7054 509.755L249.704 291.671L466.702 509.755L328.925 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dd)">
|
||||
<path d="M289.281 767.036L427.409 905.854C471.384 950.048 542.646 950.048 586.561 905.854L724.689 767.036L506.985 548.243L289.281 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M586.206 905.502L586.206 905.502C542.487 949.499 471.543 949.5 427.764 905.502L289.986 767.036L506.985 548.952L723.983 767.036L586.206 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dd)">
|
||||
<path d="M546.562 509.755L684.69 648.573C728.665 692.767 799.927 692.767 843.842 648.573L981.97 509.755L764.266 290.962L546.562 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M843.487 648.221L843.487 648.221C799.768 692.218 728.824 692.219 685.044 648.221L547.267 509.755L764.266 291.671L981.264 509.755L843.487 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dd)">
|
||||
<path d="M803.843 250.792L941.971 389.611C985.945 433.805 1057.21 433.805 1101.12 389.611L1239.25 250.792L1021.55 32L803.843 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M1100.77 389.258L1100.77 389.258C1057.05 433.256 986.105 433.256 942.325 389.258L804.548 250.792L1021.55 32.7089L1238.55 250.792L1100.77 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_dd)">
|
||||
<path d="M1062.81 509.755L1200.93 648.573C1244.91 692.767 1316.17 692.767 1360.08 648.573L1498.21 509.755L1280.51 290.962L1062.81 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M1359.73 648.221L1359.73 648.221C1316.01 692.218 1245.07 692.219 1201.29 648.221L1063.51 509.755L1280.51 291.671L1497.51 509.755L1359.73 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_dd)">
|
||||
<path d="M805.524 767.036L943.653 905.854C987.627 950.048 1058.89 950.048 1102.8 905.854L1240.93 767.036L1023.23 548.243L805.524 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M1102.45 905.502L1102.45 905.502C1058.73 949.499 987.786 949.5 944.007 905.502L806.23 767.036L1023.23 548.952L1240.23 767.036L1102.45 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="257.342" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd" x="0" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd" x="257.281" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_dd" x="514.562" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter4_dd" x="771.843" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter5_dd" x="1030.81" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter6_dd" x="773.524" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
|
@ -1,30 +0,0 @@
|
|||
import { getKcContext, type KcContext } from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>; }) {
|
||||
|
||||
const { kcContext } = getKcContext({
|
||||
mockPageId: pageId,
|
||||
storyPartialKcContext: params.kcContext
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
|
||||
<link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
|
||||
<KcApp kcContext={kcContext} />
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { createUseI18n } from "keycloakify/login";
|
||||
|
||||
export const { useI18n } = createUseI18n({
|
||||
// NOTE: Here you can override the default i18n messages
|
||||
// or define new ones that, for example, you would have
|
||||
// defined in the Keycloak admin UI for UserProfile
|
||||
// https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png
|
||||
en: {
|
||||
alphanumericalCharsOnly: "Only alphanumerical characters",
|
||||
gender: "Gender",
|
||||
// Here we overwrite the default english value for the message "doForgotPassword"
|
||||
// that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17
|
||||
doForgotPassword: "I forgot my password",
|
||||
invalidUserMessage: "Invalid username or password. (this message was overwrite in the theme)"
|
||||
},
|
||||
fr: {
|
||||
/* spell-checker: disable */
|
||||
alphanumericalCharsOnly: "Caractère alphanumérique uniquement",
|
||||
gender: "Genre",
|
||||
doForgotPassword: "J'ai oublié mon mot de passe",
|
||||
invalidUserMessage: "Nom d'utilisateur ou mot de passe invalide. (ce message a été écrasé dans le thème)"
|
||||
/* spell-checker: enable */
|
||||
}
|
||||
});
|
||||
|
||||
export type I18n = NonNullable<ReturnType<typeof useI18n>>;
|
|
@ -1,104 +0,0 @@
|
|||
import { createGetKcContext } from "keycloakify/login";
|
||||
|
||||
export type KcContextExtension =
|
||||
| { pageId: "login.ftl"; }
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
|
||||
// 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
|
||||
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
||||
| { pageId: "register.ftl"; authorizedMailDomains: string[]; };
|
||||
|
||||
//NOTE: In most of the cases you do not need to overload the KcContext, you can
|
||||
// just call createGetKcContext(...) 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 { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||
mockData: [
|
||||
{
|
||||
pageId: "login.ftl",
|
||||
locale: {
|
||||
//When we test the login page we do it in french
|
||||
currentLanguageTag: "fr",
|
||||
},
|
||||
//Uncomment the following line for hiding the Alert message
|
||||
//"message": undefined
|
||||
//Uncomment the following line for showing an Error message
|
||||
//message: { type: "error", summary: "This is an error" }
|
||||
},
|
||||
{
|
||||
pageId: "my-extra-page-2.ftl",
|
||||
someCustomValue: "foo bar baz",
|
||||
},
|
||||
{
|
||||
//NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
|
||||
pageId: "register-user-profile.ftl",
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
validators: {
|
||||
pattern: {
|
||||
pattern: "^[a-zA-Z0-9]+$",
|
||||
"ignore.empty.value": true,
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"error-message": "${alphanumericalCharsOnly}",
|
||||
},
|
||||
},
|
||||
//NOTE: To override the default mock value
|
||||
value: undefined,
|
||||
name: "username"
|
||||
},
|
||||
{
|
||||
validators: {
|
||||
options: {
|
||||
options: ["male", "female", "non_binary", "prefer_not_to_say"]
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
displayName: "${gender}",
|
||||
annotations: {},
|
||||
required: true,
|
||||
groupAnnotations: {},
|
||||
readOnly: false,
|
||||
name: "gender"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
pageId: "register.ftl",
|
||||
authorizedMailDomains: [
|
||||
"example.com",
|
||||
"another-example.com",
|
||||
"*.yet-another-example.com",
|
||||
"*.example.com",
|
||||
"hello-world.com"
|
||||
],
|
||||
// Simulate we got an error with the email field. Return text if message for given field exists.
|
||||
messagesPerField: {
|
||||
printIfExists: <T>(fieldName: string, text: T) => { console.log({ fieldName }); return fieldName === "email" ? text : undefined; },
|
||||
existsError: (fieldName: string) => fieldName === "email",
|
||||
get: (fieldName: string) => `Fake error for ${fieldName}`,
|
||||
exists: (fieldName: string) => fieldName === "email"
|
||||
},
|
||||
|
||||
}
|
||||
],
|
||||
// Defined in vite.config.ts
|
||||
// See: https://docs.keycloakify.dev/environnement-variables
|
||||
mockProperties: {
|
||||
MY_ENV_VARIABLE: "Mocked value"
|
||||
}
|
||||
});
|
||||
|
||||
export const { kcContext } = getKcContext({
|
||||
// Uncomment to test the login page for development.
|
||||
//mockPageId: "login.ftl",
|
||||
});
|
||||
|
||||
|
||||
export type KcContext = NonNullable<ReturnType<typeof getKcContext>["kcContext"]>;
|
|
@ -1,83 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "login.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/Login",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />,
|
||||
};
|
||||
|
||||
export const WithoutPasswordField: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { password: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutRegistration: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { registrationAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutRememberMe: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { rememberMe: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutPasswordReset: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { resetPasswordAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { loginWithEmailAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithPresetUsername: Story = {
|
||||
render: () => <PageStory kcContext={{ login: { username: "max.mustermann@mail.com" } }} />,
|
||||
};
|
||||
|
||||
export const WithImmutablePresetUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
attemptedUsername: "max.mustermann@mail.com",
|
||||
showUsername: true,
|
||||
},
|
||||
usernameHidden: true,
|
||||
message: { type: "info", summary: "Please re-authenticate to continue" },
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithSocialProviders: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{ 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' },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
|
@ -1,204 +0,0 @@
|
|||
import { useState, type FormEventHandler } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const my_custom_param = new URL(window.location.href).searchParams.get("my_custom_param");
|
||||
|
||||
if (my_custom_param !== null) {
|
||||
console.log("my_custom_param:", my_custom_param);
|
||||
}
|
||||
|
||||
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { social, realm, url, usernameHidden, login, auth, 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, doUseDefaultCss, classes }}
|
||||
displayInfo={
|
||||
realm.password &&
|
||||
realm.registrationAllowed &&
|
||||
!registrationDisabled
|
||||
}
|
||||
displayWide={realm.password && social.providers !== undefined}
|
||||
headerNode={msg("doLogIn")}
|
||||
infoNode={
|
||||
<div id="kc-registration">
|
||||
<span>
|
||||
{msg("noAccount")}
|
||||
<a tabIndex={6} href={url.registrationUrl}>
|
||||
{msg("doRegister")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && getClassName("kcContentWrapperClass"))}>
|
||||
<div
|
||||
id="kc-form-wrapper"
|
||||
className={clsx(
|
||||
realm.password &&
|
||||
social.providers && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
|
||||
)}
|
||||
>
|
||||
{realm.password && (
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
{!usernameHidden &&
|
||||
(() => {
|
||||
const label = !realm.loginWithEmailAllowed
|
||||
? "username"
|
||||
: realm.registrationEmailAsUsername
|
||||
? "email"
|
||||
: "usernameOrEmail";
|
||||
|
||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
|
||||
{msg(label)}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id={autoCompleteHelper}
|
||||
className={getClassName("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={getClassName("kcFormGroupClass")}>
|
||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={2}
|
||||
id="password"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
|
||||
<div id="kc-form-options">
|
||||
{realm.rememberMe && !usernameHidden && (
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input
|
||||
tabIndex={3}
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
{...(login.rememberMe === "on"
|
||||
? {
|
||||
"checked": true
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
{msg("rememberMe")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
{realm.resetPasswordAllowed && (
|
||||
<span>
|
||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
||||
{msg("doForgotPassword")}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormGroupClass")}>
|
||||
<input
|
||||
type="hidden"
|
||||
id="id-hidden-input"
|
||||
name="credentialId"
|
||||
{...(auth?.selectedCredential !== undefined
|
||||
? {
|
||||
"value": auth.selectedCredential
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("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(getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass"))}
|
||||
>
|
||||
<ul
|
||||
className={clsx(
|
||||
getClassName("kcFormSocialAccountListClass"),
|
||||
social.providers.length > 4 && getClassName("kcFormSocialAccountDoubleListClass")
|
||||
)}
|
||||
>
|
||||
{social.providers.map(p => (
|
||||
<li key={p.providerId} className={getClassName("kcFormSocialAccountListLinkClass")}>
|
||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
|
||||
<span>{p.displayName}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
//This is to show that you can create stories for pages that you haven't overloaded.
|
||||
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "login-reset-password.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/LoginResetPassword",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
infoNode={<span>footer</span>}
|
||||
>
|
||||
<form>
|
||||
{/*...*/}
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "my-extra-page-2.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/MyExtraPage2",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WitAbc: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
someCustomValue: "abc"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
// someCustomValue is declared by you in ../kcContext.ts
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
infoNode={<span>footer</span>}
|
||||
>
|
||||
|
||||
<form>
|
||||
{kcContext.someCustomValue}
|
||||
{/*...*/}
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
// ejected using 'npx eject-keycloak-page'
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}>
|
||||
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="firstName"
|
||||
defaultValue={register.formData.firstName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
|
||||
{msg("lastName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="lastName"
|
||||
defaultValue={register.formData.lastName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="email" className={getClassName("kcLabelClass")}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="email"
|
||||
defaultValue={register.formData.email ?? ""}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="username" className={getClassName("kcLabelClass")}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="username"
|
||||
defaultValue={register.formData.username ?? ""}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{passwordRequired && (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// ejected using 'npx eject-keycloak-page'
|
||||
import { useState } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileFormFields";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
displayMessage={messagesPerField.exists("global")}
|
||||
displayRequiredFields={true}
|
||||
headerNode={msg("registerTitle")}
|
||||
>
|
||||
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||
<UserProfileFormFields
|
||||
kcContext={kcContext}
|
||||
onIsFormSubmittableValueChange={setIsFormSubmittable}
|
||||
i18n={i18n}
|
||||
getClassName={getClassName}
|
||||
/>
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={getClassName("kcFormGroupClass")} style={{ "marginBottom": 30 }}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
disabled={!isFormSubmittable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "terms.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/Terms",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
|
@ -1,81 +0,0 @@
|
|||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import { Markdown } from "keycloakify/tools/Markdown";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { useDownloadTerms } from "keycloakify/login";
|
||||
|
||||
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
// NOTE: If you aren't going to customize the layout of the page you can move this hook to
|
||||
// KcApp.tsx, see: https://docs.keycloakify.dev/terms-and-conditions
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
"downloadTermMarkdown": async ({currentLanguageTag}) => {
|
||||
|
||||
const tos_url = (() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return `${import.meta.env.BASE_URL}terms/fr.md`;
|
||||
default: return `${import.meta.env.BASE_URL}terms/en.md`;
|
||||
}
|
||||
})();
|
||||
|
||||
const markdownString = await fetch(tos_url).then(response => response.text());
|
||||
|
||||
return markdownString;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const termMarkdown = evtTermMarkdown.state;
|
||||
|
||||
if (termMarkdown === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("termsTitle")}>
|
||||
<div id="kc-terms-text">
|
||||
<Markdown>{termMarkdown}</Markdown>
|
||||
</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-accept"
|
||||
type="submit"
|
||||
value={msgStr("doAccept")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
|
||||
name="cancel"
|
||||
id="kc-decline"
|
||||
type="submit"
|
||||
value={msgStr("doDecline")}
|
||||
/>
|
||||
</form>
|
||||
<div className="clearfix" />
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
import { useEffect, Fragment } from "react";
|
||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useFormValidation } from "keycloakify/login/lib/useFormValidation";
|
||||
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
|
||||
import type { I18n } from "../../i18n";
|
||||
|
||||
export type UserProfileFormFieldsProps = {
|
||||
kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
|
||||
i18n: I18n;
|
||||
getClassName: (classKey: ClassKey) => string;
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
BeforeField?: (props: { attribute: Attribute }) => JSX.Element | null;
|
||||
AfterField?: (props: { attribute: Attribute }) => JSX.Element | null;
|
||||
};
|
||||
|
||||
export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
||||
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
|
||||
|
||||
const { advancedMsg, msg } = i18n;
|
||||
|
||||
const {
|
||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||
formValidationDispatch,
|
||||
attributesWithPassword
|
||||
} = useFormValidation({
|
||||
kcContext,
|
||||
i18n
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||
}, [isFormSubmittable]);
|
||||
|
||||
let currentGroup = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{attributesWithPassword.map((attribute, i) => {
|
||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||
|
||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||
|
||||
const formGroupClassName = clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass")
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<label id={`header-${group}`} className={getClassName("kcFormGroupHeader")}>
|
||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label id={`description-${group}`} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{BeforeField && <BeforeField attribute={attribute} />}
|
||||
|
||||
<div className={formGroupClassName}>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor={attribute.name} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
{(() => {
|
||||
const { options } = attribute.validators;
|
||||
|
||||
if (options !== undefined) {
|
||||
return (
|
||||
<select
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
"action": "update value",
|
||||
"name": attribute.name,
|
||||
"newValue": event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
"action": "focus lost",
|
||||
"name": attribute.name
|
||||
})
|
||||
}
|
||||
value={value}
|
||||
>
|
||||
<>
|
||||
<option value="" selected disabled hidden>
|
||||
{msg("selectAnOption")}
|
||||
</option>
|
||||
{options.options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={(() => {
|
||||
switch (attribute.name) {
|
||||
case "password-confirm":
|
||||
case "password":
|
||||
return "password";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
})()}
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={value}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
"action": "update value",
|
||||
"name": attribute.name,
|
||||
"newValue": event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
"action": "focus lost",
|
||||
"name": attribute.name
|
||||
})
|
||||
}
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{displayableErrors.length !== 0 &&
|
||||
(() => {
|
||||
const divId = `input-error-${attribute.name}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`#${divId} > span: { display: block; }`}</style>
|
||||
<span
|
||||
id={divId}
|
||||
className={getClassName("kcInputErrorMessageClass")}
|
||||
style={{
|
||||
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
||||
}}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{AfterField && <AfterField attribute={attribute} />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
12
src/login/KcContext.ts
Normal file
12
src/login/KcContext.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/login";
|
||||
import type { KcEnvName, ThemeName } from "../kc.gen";
|
||||
|
||||
export type KcContextExtension = {
|
||||
themeName: ThemeName;
|
||||
properties: Record<KcEnvName, string> & {};
|
||||
};
|
||||
|
||||
export type KcContextExtensionPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
68
src/login/KcPage.tsx
Normal file
68
src/login/KcPage.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { Suspense, lazy } from "react";
|
||||
import type { ClassKey } from "keycloakify/login";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useDownloadTerms } from "keycloakify/login";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/login/DefaultPage";
|
||||
import Template from "keycloakify/login/Template";
|
||||
const UserProfileFormFields = lazy(
|
||||
() => import("keycloakify/login/UserProfileFormFields")
|
||||
);
|
||||
|
||||
const doMakeUserConfirmPassword = true;
|
||||
|
||||
export default function KcPage(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
downloadTermsMarkdown: async ({ currentLanguageTag }) => {
|
||||
let termsLanguageTag = currentLanguageTag;
|
||||
let termsFileName: string;
|
||||
|
||||
switch (currentLanguageTag) {
|
||||
case "fr":
|
||||
termsFileName = "fr.md";
|
||||
break;
|
||||
case "es":
|
||||
termsFileName = "es.md";
|
||||
break;
|
||||
default:
|
||||
termsFileName = "en.md";
|
||||
termsLanguageTag = "en";
|
||||
break;
|
||||
}
|
||||
|
||||
const termsMarkdown = await fetch(
|
||||
`${import.meta.env.BASE_URL}terms/${termsFileName}`
|
||||
).then(r => r.text());
|
||||
|
||||
return { termsMarkdown, termsLanguageTag };
|
||||
}
|
||||
});
|
||||
|
||||
const { i18n } = useI18n({ kcContext });
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
default:
|
||||
return (
|
||||
<DefaultPage
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
classes={classes}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = {} satisfies { [key in ClassKey]?: string };
|
42
src/login/KcPageStory.tsx
Normal file
42
src/login/KcPageStory.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import KcPage from "./KcPage";
|
||||
import { createGetKcContextMock } from "keycloakify/login/KcContext";
|
||||
import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext";
|
||||
import { themeNames, kcEnvDefaults } from "../kc.gen";
|
||||
|
||||
const kcContextExtension: KcContextExtension = {
|
||||
themeName: themeNames[0],
|
||||
properties: {
|
||||
...kcEnvDefaults
|
||||
}
|
||||
};
|
||||
const kcContextExtensionPerPage: KcContextExtensionPerPage = {};
|
||||
|
||||
export const { getKcContextMock } = createGetKcContextMock({
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage,
|
||||
overrides: {},
|
||||
overridesPerPage: {}
|
||||
});
|
||||
|
||||
export function createKcPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
const { pageId } = params;
|
||||
|
||||
function KcPageStory(props: {
|
||||
kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||
}) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return <KcPage kcContext={kcContextMock} />;
|
||||
}
|
||||
|
||||
return { KcPageStory };
|
||||
}
|
5
src/login/i18n.ts
Normal file
5
src/login/i18n.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createUseI18n } from "keycloakify/login";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
52
src/main.tsx
52
src/main.tsx
|
@ -1,35 +1,43 @@
|
|||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode, lazy, Suspense } from "react";
|
||||
import { kcContext as kcLoginThemeContext } from "./keycloak-theme/login/kcContext";
|
||||
import { kcContext as kcAccountThemeContext } from "./keycloak-theme/account/kcContext";
|
||||
|
||||
const KcLoginThemeApp = lazy(() => import("./keycloak-theme/login/KcApp"));
|
||||
const KcAccountThemeApp = lazy(() => import("./keycloak-theme/account/KcApp"));
|
||||
// Important note:
|
||||
// In this starter example we show how you can have your react app and your Keycloak theme in the same repo.
|
||||
// Most Keycloakify user only want to great a Keycloak theme.
|
||||
// If this is your case run the few commands that will remover everything that is not strictly related to the
|
||||
//Keycloak theme:
|
||||
// https://github.com/keycloakify/keycloakify-starter?tab=readme-ov-file#i-only-want-a-keycloak-theme
|
||||
const App = lazy(() => import("./App"));
|
||||
// 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";
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
window.kcContext = getKcContextMock({
|
||||
pageId: "register.ftl",
|
||||
overrides: {}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
const KcLoginThemePage = lazy(() => import("./login/KcPage"));
|
||||
const KcAccountThemePage = lazy(() => import("./account/KcPage"));
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Suspense>
|
||||
{(()=>{
|
||||
|
||||
if( kcLoginThemeContext !== undefined ){
|
||||
return <KcLoginThemeApp kcContext={kcLoginThemeContext} />;
|
||||
{(() => {
|
||||
switch (window.kcContext?.themeType) {
|
||||
case "login":
|
||||
return <KcLoginThemePage kcContext={window.kcContext} />;
|
||||
case "account":
|
||||
return <KcAccountThemePage kcContext={window.kcContext} />;
|
||||
}
|
||||
|
||||
if( kcAccountThemeContext !== undefined ){
|
||||
return <KcAccountThemeApp kcContext={kcAccountThemeContext} />;
|
||||
}
|
||||
|
||||
return <App />;
|
||||
|
||||
return <h1>No Keycloak Context</h1>;
|
||||
})()}
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
kcContext?:
|
||||
| import("./login/KcContext").KcContext
|
||||
| import("./account/KcContext").KcContext;
|
||||
}
|
||||
}
|
||||
|
|
5
src/vite-env.d.ts
vendored
5
src/vite-env.d.ts
vendored
|
@ -1,6 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.md" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
|
@ -1,57 +1,8 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
// NOTE: This is just for the Keycloakify core contributors to be able to dynamically link
|
||||
// to a local version of the keycloakify package. This is not needed for normal usage.
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { keycloakify } from "keycloakify/vite-plugin";
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
commonjs(),
|
||||
keycloakify({
|
||||
// See: https://docs.keycloakify.dev/build-options#themename
|
||||
themeName: "keycloakify-starter",
|
||||
// See: https://docs.keycloakify.dev/environnement-variables
|
||||
extraThemeProperties: [
|
||||
"MY_ENV_VARIABLE=${env.MY_ENV_VARIABLE:}"
|
||||
],
|
||||
// This is a hook that will be called after the build is done
|
||||
// but before the jar is created.
|
||||
// You can use it to add/remove/edit your theme files.
|
||||
postBuild: async keycloakifyBuildOptions => {
|
||||
|
||||
const fs = await import("fs/promises");
|
||||
const path = await import("path");
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(keycloakifyBuildOptions.keycloakifyBuildDirPath, "foo.txt"),
|
||||
Buffer.from(
|
||||
[
|
||||
"This file was created by the postBuild hook of the keycloakify vite plugin",
|
||||
"",
|
||||
"Resolved keycloakifyBuildOptions:",
|
||||
"",
|
||||
JSON.stringify(keycloakifyBuildOptions, null, 2),
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
})
|
||||
],
|
||||
/*
|
||||
* Uncomment this if you want to use the default domain provided by GitHub Pages
|
||||
* replace "keycloakify-starter" with your repository name.
|
||||
* This is only relevent if you are building an Wep App + A Keycloak theme.
|
||||
* If you are only building a Keycloak theme, you can ignore this.
|
||||
*/
|
||||
//base: "/keycloakify-starter/"
|
||||
build: {
|
||||
sourcemap: true
|
||||
}
|
||||
})
|
||||
plugins: [react(), keycloakify({})]
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue