Merge pull request #20 from keycloakify/keycloakify_10

Keycloakify 10
This commit is contained in:
Joseph Garrone 2024-06-19 01:19:11 +00:00 committed by GitHub
commit 01961cae02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 2169 additions and 6623 deletions

View File

@ -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

View File

@ -1,25 +1,27 @@
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'], extends: [
ignorePatterns: ['dist', '.eslintrc.cjs'], "eslint:recommended",
parser: '@typescript-eslint/parser', "plugin:@typescript-eslint/recommended",
plugins: ['react-refresh'], "plugin:react-hooks/recommended",
rules: { "plugin:storybook/recommended"
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
], ],
'react-hooks/exhaustive-deps': 'off', ignorePatterns: ["dist", ".eslintrc.cjs"],
'@typescript-eslint/no-redeclare': 'off', parser: "@typescript-eslint/parser",
'no-labels': 'off', 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: [ overrides: [
{ {
files: ['**/*.stories.*'], files: ["**/*.stories.*"],
rules: { rules: {
'import/no-anonymous-default-export': 'off', "import/no-anonymous-default-export": "off"
}, }
}, }
], ]
} };

View File

@ -8,15 +8,13 @@ on:
- main - main
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build-keycloak-theme
- run: npx keycloakify
check_if_version_upgraded: check_if_version_upgraded:
name: Check if version upgrade name: Check if version upgrade
@ -28,7 +26,7 @@ jobs:
to_version: ${{ steps.step1.outputs.to_version }} to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }} is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
steps: steps:
- uses: garronej/ts-ci@v2.1.0 - uses: garronej/ts-ci@v2.1.2
id: step1 id: step1
with: with:
action_name: is_package_json_version_upgraded action_name: is_package_json_version_upgraded
@ -37,73 +35,19 @@ jobs:
create_github_release: create_github_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: check_if_version_upgraded 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'
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' && github.event_name == 'push'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build-keycloak-theme
- run: npx keycloakify - uses: softprops/action-gh-release@v2
- 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: with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }} name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
tag_name: 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 }} target_commitish: ${{ github.head_ref || github.ref }}
generate_release_notes: true generate_release_notes: true
draft: false draft: false
files: | files: dist_keycloak/keycloak-theme-*.jar
retrocompat-keycloak-theme.jar
keycloak-theme.jar
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
node_modules/
/dist/
/dist_keycloak/
/public/keycloak-resources/
/.vscode/
/.yarn_home/

25
.prettierrc.json Normal file
View 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
}
}
]
}

View File

@ -2,18 +2,10 @@ import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [ addons: [],
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
],
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",
options: {}, options: {}
},
docs: {
autodocs: "tag",
}, },
staticDirs: ["../public"] staticDirs: ["../public"]
}; };

View 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>

View File

@ -5,10 +5,10 @@ const preview: Preview = {
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
date: /Date$/i, date: /Date$/i
}, }
}, }
}, }
}; };
export default preview; export default preview;

View File

@ -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
View File

@ -1,229 +1,47 @@
<p align="center"> <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/>
<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> </p>
# Introduction This starter is based on Vite. There is also [a Webpack based starter](https://github.com/keycloakify/keycloakify-starter-webpack).
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.
# Quick start # Quick start
```bash ```bash
git clone https://github.com/keycloakify/keycloakify-starter git clone https://github.com/keycloakify/keycloakify-starter
cd keycloakify-starter cd keycloakify-starter
yarn install # Or use an other package manager, just be sure to delete the yarn.lock if you do.
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}
``` ```
## 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. [Documentation](https://docs.keycloakify.dev/v/v10/testing-your-theme)
Checkout [this video](https://www.youtube.com/watch?v=cB86HE_HIDc) to understand dev containers and how to set up your environment.
# Theme variant # How to customize the theme
Keycloakify enables you to create different variant for a single theme. [Documentation](https://docs.keycloakify.dev/v/v10/customization-strategies)
This enable you to have a single jar that embed two or more theme variant.
![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. You need to have Maven installed to build the theme (The `mvn` command must be in the PATH).
[See documentation](https://docs.keycloakify.dev/build-options#themename) - On macOS: `brew install maven`
- On Debian/Ubuntu: `sudo apt-get install maven`
# The CI workflow - On Windows: `choco install openjdk` and `choco install maven` (Or download from [here](https://maven.apache.org/download.cgi))
- 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)
```bash ```bash
yarn npm run build-keycloak-theme
yarn storybook
``` ```
# 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 The starter comes with a generic GitHub Actions workflow that builds the theme and publishes
docker build -f Dockerfile -t keycloakify/keycloakify-starter:main . the jars [as GitHub releases artifacts](https://github.com/keycloakify/keycloakify-starter/releases/tag/v7.1.0).
docker run -it -dp 8083:80 keycloakify/keycloakify-starter:main To release a new version **just update the `package.json` version and push**.
# You can access the app at http://localhost:8083
```
# 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.

View File

@ -1,78 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@ -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";
}
}
}

View File

@ -1,8 +1,7 @@
{ {
"name": "keycloakify-starter", "name": "keycloakify-starter",
"homepage": "https://starter.keycloakify.dev",
"version": "6.1.10", "version": "6.1.10",
"description": "A starter/demo project for keycloakify", "description": "Starter for Keycloakify 10",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/codegouvfr/keycloakify-starter.git" "url": "git://github.com/codegouvfr/keycloakify-starter.git"
@ -11,32 +10,22 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build-keycloak-theme": "yarn build && keycloakify", "build-keycloak-theme": "npm run build && keycloakify build",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"format": "npx prettier . --write"
}, },
"author": "u/garronej",
"license": "MIT", "license": "MIT",
"keywords": [], "keywords": [],
"dependencies": { "dependencies": {
"evt": "^2.5.7", "keycloakify": "10.0.0-rc.72",
"keycloakify": "^9.6.6",
"oidc-spa": "^4.6.2",
"powerhooks": "^1.0.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0"
"tsafe": "^1.6.6",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^8.0.2", "storybook": "^8.1.6",
"@storybook/addon-interactions": "^8.0.2", "@storybook/react": "^8.1.6",
"@storybook/addon-links": "^8.0.2", "@storybook/react-vite": "^8.1.6",
"@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": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
@ -46,13 +35,8 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-storybook": "^0.8.0", "eslint-plugin-storybook": "^0.8.0",
"storybook": "^8.0.2", "prettier": "3.3.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.8", "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"
} }
} }

View File

@ -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.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1,7 +0,0 @@
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>

View File

@ -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 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.
- 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
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). - **Data Security**: Users are responsible for safeguarding their login credentials and should not disclose their passwords to any third party.
INSEE - **Acceptable Use**: Users are expected to use the Service in a responsible manner that does not infringe upon the rights of others.
88 avenue Verdier - **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.
CS 70058
92541 Montrouge cedex
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 These Terms shall be governed and construed in accordance with the laws of [Your Country], without regard to its conflict of law provisions.
Internet. The use of a computer is recommended. Use of the datalab services is free.
The user community is accessible on: ## Contact Information
- Tchap, salon [SSP Cloud](https://www.tchap.gouv.fr/#/room/#SSPCloudXDpAw6v:agent.finances.tchap.gouv.fr) 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).
- Rocket Chat at MIM Libre, [SSP Cloud lounge](https://chat.mim-libre.fr/channel/sspcloud)
## Limits of use of the Service ## Changes to Terms of Service
Public data and data can be processed on the datalab 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.
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.).
[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 ??] ## Effective Date
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.
## Roles, commitments and associated responsibilities These terms are effective as of **[Insert Date]**.
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

49
public/terms/es.md Normal file
View 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]**.

View File

@ -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). Lhé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] **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 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.
Le SSP Cloud sadresse 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_ ## Inscription au Compte
- 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
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). ## Propriété Intellectuelle
Insee
88 avenue Verdier
CS 70058
92541 Montrouge cedex
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 dutilisation 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 nimporte quel navigateur connecté à ## Informations de Contact
Internet. L'utilisation d'un ordinateur est recommandée. Lutilisation des services du datalab est gratuite.
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) ## Modifications des Conditions de Service
- Rocket Chat du MIM Libre, salon [SSP Cloud](https://chat.mim-libre.fr/channel/sspcloud)
## Limites dutilisation 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 ## Date d'Effet
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..).
[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??] Ces conditions sont effectives à partir du **[Insérer la Date]**.
Le caractère protégé ou sensible des informations stockées ou traitées sur le datalab
est soumis à lappréciation de lutilisateur 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 sappuie sur des technologies open source de référence. Toutefois, il nest pas garanti quil
soit exempt danomalies 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 quils
soient, qui pourraient être causés à la suite dun 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 lensemble 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 lexé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 lentité
administrative dont est issu lutilisateur. 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), lexploitation de la plateforme (suivi,
statistiques dusages) 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
lutilisation dun 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 nont 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 dutilisation 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 simposent à lutilisateur 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 daccueil).
## 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 daccès CNIL pour : <innovation@insee.fr>

View File

@ -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);
}
}

View File

@ -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>
&nbsp;&nbsp;&nbsp;
<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" />
&nbsp;&nbsp;&nbsp;
<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>
);
}

View File

@ -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.0 KiB

View File

@ -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

View File

@ -1,4 +0,0 @@
import App from "./App";
export * from "./App";
export default App;

View File

@ -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
View 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
View 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 };

View 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
View 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
View 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 */

View File

@ -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.

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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

View File

@ -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 };
}

View File

@ -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>>;

View File

@ -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"]>;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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" }
}}
/>
};

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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

View File

@ -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 };
}

View File

@ -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>>;

View File

@ -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"]>;

View File

@ -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' },
],
},
}}
/>
),
};

View File

@ -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>
);
}

View File

@ -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 }
}}
/>
)
};

View File

@ -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>
);
}

View File

@ -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"
}}
/>
)
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 />
};

View File

@ -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>
);
}

View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
import { createUseI18n } from "keycloakify/login";
export const { useI18n, ofTypeI18n } = createUseI18n({});
export type I18n = typeof ofTypeI18n;

View File

@ -1,35 +1,43 @@
/* eslint-disable react-refresh/only-export-components */
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { StrictMode, lazy, Suspense } from "react"; 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")); // The following block can be uncommented to test a specific page with `yarn dev`
const KcAccountThemeApp = lazy(() => import("./keycloak-theme/account/KcApp")); // Don't forget to comment back or your bundle size will increase
// Important note: /*
// In this starter example we show how you can have your react app and your Keycloak theme in the same repo. import { getKcContextMock } from "./login/KcPageStory";
// 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 if (import.meta.env.DEV) {
//Keycloak theme: window.kcContext = getKcContextMock({
// https://github.com/keycloakify/keycloakify-starter?tab=readme-ov-file#i-only-want-a-keycloak-theme pageId: "register.ftl",
const App = lazy(() => import("./App")); overrides: {}
});
}
*/
const KcLoginThemePage = lazy(() => import("./login/KcPage"));
const KcAccountThemePage = lazy(() => import("./account/KcPage"));
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<Suspense> <Suspense>
{(()=>{ {(() => {
switch (window.kcContext?.themeType) {
if( kcLoginThemeContext !== undefined ){ case "login":
return <KcLoginThemeApp kcContext={kcLoginThemeContext} />; return <KcLoginThemePage kcContext={window.kcContext} />;
case "account":
return <KcAccountThemePage kcContext={window.kcContext} />;
} }
return <h1>No Keycloak Context</h1>;
if( kcAccountThemeContext !== undefined ){
return <KcAccountThemeApp kcContext={kcAccountThemeContext} />;
}
return <App />;
})()} })()}
</Suspense> </Suspense>
</StrictMode> </StrictMode>
); );
declare global {
interface Window {
kcContext?:
| import("./login/KcContext").KcContext
| import("./account/KcContext").KcContext;
}
}

5
src/vite-env.d.ts vendored
View File

@ -1,6 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module "*.md" {
const src: string;
export default src;
}

View File

@ -1,57 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' 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 { keycloakify } from "keycloakify/vite-plugin"; import { keycloakify } from "keycloakify/vite-plugin";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [react(), keycloakify({})]
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
}
})

4826
yarn.lock

File diff suppressed because it is too large Load Diff