Compare commits

..

135 Commits

Author SHA1 Message Date
25920c208d Release candidate 2024-10-19 22:33:34 +02:00
19da96113f Don't export internals 2024-10-19 22:33:34 +02:00
6e584e809e Merge branch 'main' into decouple_userprofile_logic 2024-10-19 22:29:48 +02:00
4185188a5b Release candidate 2024-10-19 22:28:10 +02:00
4273322ed5 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:30 +02:00
ba0532c95d docs: update README.md [skip ci] 2024-10-19 22:27:30 +02:00
3a2fe597ba Bump version 2024-10-19 22:27:30 +02:00
dda77952a0 #694 Probably some shell handle double quote differently 2024-10-19 22:27:30 +02:00
d2e518d96b #693 #692 2024-10-19 22:27:30 +02:00
f3a97b2538 Bump version 2024-10-19 22:27:29 +02:00
cacd017244 #696 2024-10-19 22:27:29 +02:00
f5b15a5ef6 Fix Phase two links 2024-10-19 22:27:29 +02:00
de620dca56 Fix light mode rendering 2024-10-19 22:27:29 +02:00
8decf4a3c9 Add phaseTwo as sponsor 2024-10-19 22:27:29 +02:00
831326952b Resize zone2 logo 2024-10-19 22:27:29 +02:00
27da578446 Bump version 2024-10-19 22:27:29 +02:00
2c1cca168f Resolve package.json path relative to the package.json 2024-10-19 22:27:29 +02:00
e498fb784b Bump version 2024-10-19 22:27:29 +02:00
2917719315 Add dir=rtl attribut to html when using a RTL language 2024-10-19 22:27:29 +02:00
9ed90995e4 typesafety fix 2024-10-19 22:27:29 +02:00
0f99bb5bdc fix: added parameter type for story context on register page 2024-10-19 22:27:29 +02:00
1f4d4473e4 Bump version 2024-10-19 22:27:29 +02:00
5332001ff4 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:29 +02:00
22241fd7ad docs: update README.md [skip ci] 2024-10-19 22:27:29 +02:00
ddeade9775 Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-10-19 22:27:29 +02:00
f1cb165bdd Bump version 2024-10-19 22:27:29 +02:00
9873353990 Fix initialize-email-theme 2024-10-19 22:27:29 +02:00
b879569b81 Announcement about Keycloak 26 2024-10-19 22:27:29 +02:00
c3e821088b Bump version 2024-10-19 22:27:29 +02:00
dc4f386e7a Fix vite quitting if custom handler implemented 2024-10-19 22:27:29 +02:00
a40810b364 Bump version 2024-10-19 22:27:29 +02:00
1690629717 Fix: check for delegation of the eject-page command 2024-10-19 22:27:29 +02:00
9a6a71c8bc Fix litle inconsistency 2024-10-19 22:27:29 +02:00
d626699f08 Bump version 2024-10-19 22:27:29 +02:00
6aa60e685b Release candidate 2024-10-19 22:27:29 +02:00
9910762abc Add initialize-email-theme, initialize-account-theme and copy-keycloak-resources-to-public to commands that can be delegated to a custom handler 2024-10-19 22:27:29 +02:00
182fb430f1 Fix dead code 2024-10-19 22:27:29 +02:00
bda20e2fbe Release candidate 2024-10-19 22:27:29 +02:00
bc586eceef Make sure the update-kc-gen command is delegated when building with vite 2024-10-19 22:27:29 +02:00
128b27221a Release candidate 2024-10-19 22:27:29 +02:00
2dfb4eda9d No need to handle non react environement with custom handler support 2024-10-19 22:27:29 +02:00
fed6af4dfe Release candidate 2024-10-19 22:27:29 +02:00
c4ee6cd85c Fix not handling correctly exit cause 2024-10-19 22:27:29 +02:00
8fc307bd8d Release candidate 2024-10-19 22:27:29 +02:00
9e9ffcd586 add debug logs 2024-10-19 22:27:29 +02:00
49b064b5f2 Release candidate 2024-10-19 22:27:29 +02:00
ef6f5a4c23 Add other missing declaration files 2024-10-19 22:27:28 +02:00
e92562fd44 Release candidate 2024-10-19 22:27:28 +02:00
fe65ddb5f8 Fix missing exports 2024-10-19 22:27:28 +02:00
ffd405c6db Release candidate 2024-10-19 22:27:28 +02:00
9e41868e0d Implement custom handler cli hook 2024-10-19 22:27:28 +02:00
ca6accc889 Bump version 2024-10-19 22:27:28 +02:00
dfe2e1562a Fix cache issue 2024-10-19 22:27:28 +02:00
ab43bb73d7 Bump version 2024-10-19 22:27:28 +02:00
22b0b95e54 Update readme, support keycloak 26 2024-10-19 22:27:28 +02:00
290ad8b592 Update version ranges for Multi-Page account theme 2024-10-19 22:27:28 +02:00
d5519dbb55 Release candidate 2024-10-19 22:27:28 +02:00
4de9e059e9 Aditional context exclusion 2024-10-19 22:27:28 +02:00
e573aff6ae Release candidate 2024-10-19 22:27:28 +02:00
908e083dee Update version target range 2024-10-19 22:27:28 +02:00
ec29724997 Fix link in CONTRIBUTING.md 2024-10-19 22:27:28 +02:00
88756e9807 Bump version 2024-10-19 22:27:28 +02:00
80d8a0c4e3 ['select-radiobuttons'/'multiselect-checkboxes'] fixed 'inputOptionLabels' 2024-10-19 22:27:28 +02:00
7241f0c741 Bump version 2024-10-19 22:27:28 +02:00
8565eb3fb8 Update tsafe 2024-10-19 22:27:28 +02:00
87198f6e56 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
fa934da442 docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
6c4dc711d2 Put Kathi as first contributor 2024-10-19 22:27:28 +02:00
1f2a755a97 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
a0e3dc163a docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
810dc6ceb5 Bump version 2024-10-19 22:27:28 +02:00
7203c742be Avoid modifying BASE_URL for App context 2024-10-19 22:27:28 +02:00
2fd04cfb61 Bump version 2024-10-19 22:27:28 +02:00
9c44d13f73 Update tsafe (provide ESM distribution) 2024-10-19 22:27:27 +02:00
d6436a58a2 update ci 2024-10-19 22:27:27 +02:00
613167f3a6 Bump version 2024-10-19 22:27:27 +02:00
ab0c281d98 Fix allegated vulnerability 2024-10-19 22:27:27 +02:00
c84dc281a2 Bump version 2024-10-19 22:27:27 +02:00
835833a61b Remove unessesary reference to react specific construct in KcContext 2024-10-19 22:27:27 +02:00
9af542ec89 Bump version 2024-10-19 22:27:27 +02:00
06e33196bb Refactor: Make ClassKey importable without having react as a dependency 2024-10-19 22:27:27 +02:00
36dd324139 complete decoupling of user profile form validation logic 2024-10-19 10:18:22 +02:00
52d4fe920c Merge pull request #697 from keycloakify/all-contributors/add-marvinruder
docs: add marvinruder as a contributor for bug
2024-10-19 03:21:05 +02:00
0d090d50d4 Bump version 2024-10-19 02:28:32 +02:00
e57232edde #694 Probably some shell handle double quote differently 2024-10-19 02:28:11 +02:00
dfe1e7ddd1 docs: update .all-contributorsrc [skip ci] 2024-10-19 00:24:37 +00:00
5ffc42c9db docs: update README.md [skip ci] 2024-10-19 00:24:36 +00:00
c63648a1b0 #693 #692 2024-10-19 02:19:41 +02:00
80fd4095c4 Bump version 2024-10-17 23:23:52 +02:00
31d7a938f2 #696 2024-10-17 23:23:26 +02:00
ee1b6868f8 Fix Phase two links 2024-10-17 19:54:14 +02:00
7c7e5544e4 Fix light mode rendering 2024-10-16 05:13:52 +02:00
06fe26fbe7 Add phaseTwo as sponsor 2024-10-16 04:10:17 +02:00
c932c7d8f6 Resize zone2 logo 2024-10-16 03:37:00 +02:00
d56c536446 Bump version 2024-10-13 00:55:23 +02:00
f5a9a28124 Resolve package.json path relative to the package.json 2024-10-13 00:55:06 +02:00
b86039536e Bump version 2024-10-12 17:33:44 +02:00
59c4675e8a Add dir=rtl attribut to html when using a RTL language 2024-10-12 17:30:30 +02:00
fbf6a329df typesafety fix 2024-10-11 23:55:07 +02:00
ddec3118a4 Merge pull request #688 from keycloakify/fix/missing-type-register-story
fix: added parameter type for story context on register page
2024-10-08 23:58:06 +02:00
94e2786297 fix: added parameter type for story context on register page 2024-10-08 16:50:11 -05:00
ecfdff5454 Bump version 2024-10-07 23:53:05 +02:00
c598a58ec9 Merge pull request #685 from keycloakify/all-contributors/add-nima70
docs: add nima70 as a contributor for code, and test
2024-10-07 23:47:25 +02:00
3e38beb190 docs: update .all-contributorsrc [skip ci] 2024-10-07 21:47:11 +00:00
61a86e8e82 docs: update README.md [skip ci] 2024-10-07 21:47:10 +00:00
9c8e127fa0 Merge pull request #672 from nima70/main
Changes:
2024-10-07 23:46:13 +02:00
eb23b40e5c Bump version 2024-10-07 21:03:04 +02:00
9e19aafcd0 Fix initialize-email-theme 2024-10-07 21:02:51 +02:00
4cdf26b6f9 Announcement about Keycloak 26 2024-10-07 20:56:03 +02:00
88de58cc22 Bump version 2024-10-06 22:56:42 +02:00
f6b48c88b9 Fix vite quitting if custom handler implemented 2024-10-06 22:55:18 +02:00
12534e57ad Bump version 2024-10-06 22:09:21 +02:00
52e33bba2d Fix: check for delegation of the eject-page command 2024-10-06 22:08:43 +02:00
d63e5f4e54 Fix litle inconsistency 2024-10-06 15:37:32 +02:00
8d31866a0b Bump version 2024-10-06 15:09:53 +02:00
7d818f217a Merge pull request #683 from keycloakify/feat_custom_handler
Feat custom handler
2024-10-06 15:09:26 +02:00
7156665684 Release candidate 2024-10-06 13:19:12 +02:00
5045c5e8bf Add initialize-email-theme, initialize-account-theme and copy-keycloak-resources-to-public to commands that can be delegated to a custom handler 2024-10-06 13:18:30 +02:00
9de2ed9eaf Fix dead code 2024-10-06 12:44:46 +02:00
096cf7a570 Release candidate 2024-10-06 09:07:10 +02:00
a04f07d149 Make sure the update-kc-gen command is delegated when building with vite 2024-10-06 09:06:49 +02:00
63775b2866 Release candidate 2024-10-06 06:45:06 +02:00
e8609de7b4 No need to handle non react environement with custom handler support 2024-10-06 06:44:53 +02:00
e62aa89d72 Release candidate 2024-10-06 06:42:04 +02:00
77f12a940d Fix not handling correctly exit cause 2024-10-06 06:41:51 +02:00
0fe49e3d6e Release candidate 2024-10-05 22:29:13 +02:00
881386a123 add debug logs 2024-10-05 22:28:36 +02:00
7b9aec4ed0 Release candidate 2024-10-05 21:39:32 +02:00
cf18f9d06c Add other missing declaration files 2024-10-05 21:39:14 +02:00
052936f769 Release candidate 2024-10-05 21:23:57 +02:00
590de7a67b Fix missing exports 2024-10-05 21:23:17 +02:00
7f608ad8ad Release candidate 2024-10-05 20:31:41 +02:00
35b012b937 Implement custom handler cli hook 2024-10-05 20:30:09 +02:00
5892cf2ba7 Decouple user profile form logic so it can be consumed in angular 2024-09-30 00:19:37 +02:00
c9d7fc1b6e Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-09-29 04:35:02 -04:00
48 changed files with 2866 additions and 1734 deletions

View File

@ -290,6 +290,25 @@
"code",
"test"
]
},
{
"login": "nima70",
"name": "Nima Shokouhfar",
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
"profile": "https://github.com/nima70",
"contributions": [
"code",
"test"
]
},
{
"login": "marvinruder",
"name": "Marvin A. Ruder",
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
"profile": "https://mruder.dev",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@ -43,21 +43,48 @@
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
> 📣 **Keycloakify 26 Released**
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
## Sponsors
Friends for the project, we trust and recommend their services.
Project backers, we trust and recommend their services.
<br/>
<div align="center">
![Logo Dark](https://github.com/user-attachments/assets/088f6631-b7ef-42ad-812b-df4870dc16ae#gh-dark-mode-only)
![Logo Dark](https://github.com/user-attachments/assets/d8f6b6f5-3de4-4adc-ba15-cb4074e8309b#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/53fb16f8-02ef-4523-9c36-b42d6e59837e#gh-light-mode-only)
![Logo Light](https://github.com/user-attachments/assets/20736d6f-f22d-4a9d-9dfe-93be209a8191#gh-light-mode-only)
</div>
<br/>
<p align="center">
<i><a href="https://phasetwo.io/?utm_source=keycloakify"><strong>Keycloak as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-?utm_source=keycloakify">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/?utm_source=keycloakify">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/?utm_source=keycloakify">Keycloak support</a> to businesses of all sizes.</i>
</p>
<br/>
<br/>
<br/>
<div align="center">
![Logo Dark](https://github.com/user-attachments/assets/dd3925fb-a58a-4e91-b360-69c2fa1f1087#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/6c00c201-eed7-485a-a887-70891559d69b#gh-light-mode-only)
</div>
@ -82,7 +109,7 @@ Friends for the project, we trust and recommend their services.
</div>
<p align="center">
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. </a>
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company. </a>
<br/>
Use code <code>keycloakify5</code> at checkout for a 5% discount.
</p>
@ -136,6 +163,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/oes-rioniz/"><img src="https://avatars.githubusercontent.com/u/5172296?v=4?s=100" width="100px;" alt="Omid"/><br /><sub><b>Omid</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kathari00"><img src="https://avatars.githubusercontent.com/u/42547712?v=4?s=100" width="100px;" alt="Katharina Eiserfey"/><br /><sub><b>Katharina Eiserfey</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luca-peruzzo"><img src="https://avatars.githubusercontent.com/u/69015314?v=4?s=100" width="100px;" alt="Luca Peruzzo"/><br /><sub><b>Luca Peruzzo</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nima70"><img src="https://avatars.githubusercontent.com/u/5094767?v=4?s=100" width="100px;" alt="Nima Shokouhfar"/><br /><sub><b>Nima Shokouhfar</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mruder.dev"><img src="https://avatars.githubusercontent.com/u/18495294?v=4?s=100" width="100px;" alt="Marvin A. Ruder"/><br /><sub><b>Marvin A. Ruder</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/issues?q=author%3Amarvinruder" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "11.2.10",
"version": "11.3.9-rc.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
@ -38,12 +38,14 @@
"dist/",
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/",
"dist/bin/**/*.d.ts",
"dist/bin/main.js",
"dist/bin/*.index.js",
"dist/bin/*.node",
"dist/bin/shared/constants.js",
"dist/bin/shared/*.d.ts",
"dist/bin/shared/*.js.map",
"dist/bin/shared/constants.js.map",
"dist/bin/shared/customHandler.js",
"dist/bin/shared/customHandler.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts",
@ -71,7 +73,6 @@
"@babel/preset-env": "7.24.8",
"@babel/types": "^7.24.5",
"@emotion/react": "^11.11.4",
"@keycloakify/angular": "^0.0.1-rc.19",
"@octokit/rest": "^20.1.1",
"@storybook/addon-a11y": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.13",

View File

@ -13,16 +13,11 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
console.log(chalk.cyan("Theme type:"));

View File

@ -1,13 +1,96 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import { getBuildContext } from "./shared/buildContext";
import type { CliCommandOptions } from "./main";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import type { BuildContext } from "./shared/buildContext";
import { transformCodebase } from "./tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
copyKeycloakResourcesToPublic({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
if (hasBeenHandled) {
return;
}
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -20,17 +20,22 @@ import {
} from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildContext = getBuildContext({
cliCommandOptions
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "eject-page",
buildContext
});
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
@ -244,12 +249,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
)} copy pasted from the Keycloakify source code into your project`
);
edit_KcApp: {
edit_KcPage: {
if (
pageIdOrComponent !== templateValue &&
pageIdOrComponent !== userProfileFormFieldsValue
) {
break edit_KcApp;
break edit_KcPage;
}
const kcAppTsxPath = pathJoin(

View File

@ -1,17 +1,24 @@
import { getBuildContext } from "../shared/buildContext";
import type { CliCommandOptions } from "../main";
import type { BuildContext } from "../shared/buildContext";
import cliSelect from "cli-select";
import child_process from "child_process";
import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { generateKcGenTs } from "../shared/generateKcGenTs";
import { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
@ -97,7 +104,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
await generateKcGenTs({
await updateKcGenCommand({
buildContext: {
...buildContext,
implementedThemeTypes: {

View File

@ -5,15 +5,18 @@ import * as fs from "fs";
import chalk from "chalk";
import { z } from "zod";
import { id } from "tsafe/id";
import { is } from "tsafe/is";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
projectDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function updateAccountThemeImplementationInConfig(params: {
buildContext: BuildContext;
buildContext: BuildContextLike;
accountThemeType: "Single-Page" | "Multi-Page";
}) {
const { buildContext, accountThemeType } = params;
@ -81,6 +84,8 @@ export function updateAccountThemeImplementationInConfig(params: {
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();

View File

@ -1,15 +1,25 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext";
import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
import type { CliCommandOptions } from "./main";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import fetch from "make-fetch-happen";
import { SemVer } from "./tools/SemVer";
import { assert } from "tsafe/assert";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
@ -29,7 +39,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log("Initialize with the base email theme from which version of Keycloak?");
const { keycloakVersion } = await promptKeycloakVersion({
let { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary
startingFromMajor: 17,
excludeMajorVersions: [],
@ -37,8 +47,32 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
buildContext
});
const getUrl = (keycloakVersion: string) => {
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
};
keycloakVersion = await (async () => {
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
while (true) {
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
const response = await fetch(url, buildContext.fetchOptions);
if (response.ok) {
break;
}
assert(keycloakVersionParsed.patch !== 0);
keycloakVersionParsed.patch--;
}
return SemVer.stringify(keycloakVersionParsed);
})();
const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
url: getUrl(keycloakVersion),
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",

View File

@ -2,19 +2,16 @@ import { generateResources } from "./generateResources";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process";
import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext";
import type { BuildContext } from "../shared/buildContext";
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main";
import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os";
import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildContext = getBuildContext({ cliCommandOptions });
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
exit_if_maven_not_installed: {
let commandOutput: Buffer | undefined = undefined;

View File

@ -4,8 +4,9 @@ import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
import { getBuildContext } from "./shared/buildContext";
export type CliCommandOptions = {
type CliCommandOptions = {
projectDirPath: string | undefined;
};
@ -69,10 +70,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./keycloakify");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -130,10 +131,13 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
const { command } = await import("./start-keycloak");
await command({ cliCommandOptions });
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
});
}
});
@ -144,10 +148,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./eject-page");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -158,10 +162,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./add-story");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -172,10 +176,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-email-theme");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -186,10 +190,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-account-theme");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -201,10 +205,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
@ -216,10 +220,10 @@ program
})
.task({
skip,
handler: async cliCommandOptions => {
handler: async ({ projectDirPath }) => {
const { command } = await import("./update-kc-gen");
await command({ cliCommandOptions });
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});

View File

@ -7,7 +7,6 @@ import {
dirname as pathDirname
} from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import type { CliCommandOptions } from "../main";
import { z } from "zod";
import * as fs from "fs";
import { assert, type Equals } from "tsafe/assert";
@ -24,7 +23,7 @@ import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
import { is } from "tsafe/is";
export type BuildContext = {
@ -43,7 +42,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
assetsDirPath: string;
fetchOptions: ProxyFetchOptions;
fetchOptions: FetchOptionsLike;
kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
@ -129,14 +128,12 @@ export type ResolvedViteConfig = {
};
export function getBuildContext(params: {
cliCommandOptions: CliCommandOptions;
projectDirPath: string | undefined;
}): BuildContext {
const { cliCommandOptions } = params;
const projectDirPath =
cliCommandOptions.projectDirPath !== undefined
params.projectDirPath !== undefined
? getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.projectDirPath,
pathIsh: params.projectDirPath,
cwd: process.cwd()
})
: process.cwd();
@ -511,6 +508,15 @@ export function getBuildContext(params: {
return themeNames;
})();
const relativePathsCwd = (() => {
switch (bundler) {
case "vite":
return projectDirPath;
case "webpack":
return pathDirname(packageJsonFilePath);
}
})();
const projectBuildDirPath = (() => {
webpack: {
if (bundler !== "webpack") {
@ -522,7 +528,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -566,7 +572,7 @@ export function getBuildContext(params: {
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.keycloakifyBuildDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -595,7 +601,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -667,7 +673,7 @@ export function getBuildContext(params: {
pathIsh:
parsedPackageJson.keycloakify
.staticDirPathInProjectBuildDirPath,
cwd: projectBuildDirPath
cwd: relativePathsCwd
});
}
@ -995,7 +1001,7 @@ export function getBuildContext(params: {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
cwd: projectDirPath
cwd: relativePathsCwd
})
};
}
@ -1005,7 +1011,7 @@ export function getBuildContext(params: {
? undefined
: getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
cwd: projectDirPath
cwd: relativePathsCwd
}),
port: buildOptions.startKeycloakOptions?.port
}

View File

@ -71,3 +71,8 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en";
export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};

View File

@ -1,95 +0,0 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { assert } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "../tools/fs.rmSync";
import type { BuildContext } from "./buildContext";
import { transformCodebase } from "../tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = {
publicDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function copyKeycloakResourcesToPublic(params: {
buildContext: BuildContextLike;
}) {
const { buildContext } = params;
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -0,0 +1,41 @@
import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
export const BIN_NAME = "_keycloakify-custom-handler";
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
export type CommandName =
| "update-kc-gen"
| "eject-page"
| "add-story"
| "initialize-account-theme"
| "initialize-email-theme"
| "copy-keycloak-resources-to-public";
export type ApiVersion = "v1";
export function readParams(params: { apiVersion: ApiVersion }) {
const { apiVersion } = params;
assert(apiVersion === "v1");
const commandName = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
assert(envValue !== undefined);
return envValue as CommandName;
})();
const buildContext = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
assert(envValue !== undefined);
return JSON.parse(envValue) as BuildContext;
})();
return { commandName, buildContext };
}

View File

@ -0,0 +1,46 @@
import { assert, type Equals } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
import {
NOT_IMPLEMENTED_EXIT_CODE,
type CommandName,
BIN_NAME,
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { dirname as pathDirname } from "path";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
return { hasBeenHandled: false };
}
try {
child_process.execSync(`npx ${BIN_NAME}`, {
stdio: "inherit",
env: {
...process.env,
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
}
});
} catch (error: any) {
const status = error.status;
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
return { hasBeenHandled: false };
}
process.exit(status);
}
return { hasBeenHandled: true };
}

View File

@ -1,175 +0,0 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { BuildContext } from "./buildContext";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import { z } from "zod";
export type BuildContextLike = {
projectDirPath: string;
themeNames: string[];
environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
implementedThemeTypes: Pick<
BuildContext["implementedThemeTypes"],
"login" | "account"
>;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateKcGenTs(params: {
buildContext: BuildContextLike;
}): Promise<void> {
const { buildContext } = params;
const isReactProject: boolean = await (async () => {
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return zParsedPackageJson.parse(
JSON.parse(
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
)
);
})();
return (
{
...parsedPackageJson.dependencies,
...parsedPackageJson.devDependencies
}.react !== undefined
);
})();
const filePath = pathJoin(
buildContext.themeSrcDirPath,
`kc.gen.ts${isReactProject ? "x" : ""}`
);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
...(!isReactProject
? []
: [
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`
]),
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
if (!isReactProject) {
break delete_legacy_file;
}
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
}

View File

@ -1,6 +1,5 @@
import { getBuildContext } from "../shared/buildContext";
import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { CONTAINER_NAME } from "../shared/constants";
import { SemVer } from "../tools/SemVer";
@ -29,13 +28,14 @@ import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
export type CliCommandOptions = CliCommandOptions_common & {
port: number | undefined;
keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined;
};
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
export async function command(params: {
buildContext: BuildContext;
cliCommandOptions: {
port: number | undefined;
keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined;
};
}) {
exit_if_docker_not_installed: {
let commandOutput: string | undefined = undefined;
@ -88,9 +88,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
process.exit(1);
}
const { cliCommandOptions } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const { cliCommandOptions, buildContext } = params;
const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
@ -398,12 +396,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
...(realmJsonFilePath === undefined
? []
: [
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
]),
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map(
jarFilePath =>
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
),
...(keycloakMajorVersionNumber <= 20
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
@ -426,7 +424,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}))
.map(
({ localDirPath, containerDirPath }) =>
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
),
...buildContext.environmentVariables
.map(({ name }) => ({ name, envValue: process.env[name] }))

View File

@ -1,16 +1,18 @@
import { type FetchOptions } from "make-fetch-happen";
import * as child_process from "child_process";
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
export type ProxyFetchOptions = Pick<
FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>;
export type FetchOptionsLike = {
proxy: string | undefined;
noProxy: string | string[];
strictSSL: boolean;
cert: string | string[] | undefined;
ca: string[] | undefined;
};
export function getProxyFetchOptions(params: {
npmConfigGetCwd: string;
}): ProxyFetchOptions {
}): FetchOptionsLike {
const { npmConfigGetCwd } = params;
const cfg = (() => {

View File

@ -1,13 +1,120 @@
import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext";
import { generateKcGenTs } from "./shared/generateKcGenTs";
import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "./tools/fs.existsAsync";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildContext = getBuildContext({
cliCommandOptions
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
});
await generateKcGenTs({ buildContext });
if (hasBeenHandled) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
`import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
}

View File

@ -94,6 +94,7 @@ export declare namespace KcContext {
languageTag: string;
}[];
currentLanguageTag: string;
rtl?: boolean;
};
auth?: {
showUsername?: boolean;

View File

@ -19,6 +19,7 @@ export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
rtl?: boolean;
};
"x-keycloakify": {
messages: Record<string, string>;
@ -95,6 +96,49 @@ export function createGetI18n<
const html = document.querySelector("html");
assert(html !== null);
html.lang = currentLanguageTag;
const isRtl = (() => {
const { rtl } = kcContext.locale ?? {};
if (rtl !== undefined) {
return rtl;
}
return [
/* spell-checker: disable */
// Common RTL languages
"ar", // Arabic
"fa", // Persian (Farsi)
"he", // Hebrew
"ur", // Urdu
"ps", // Pashto
"syr", // Syriac
"dv", // Divehi (Maldivian)
"ku", // Kurdish (Sorani)
"ug", // Uighur
"az", // Azerbaijani (Arabic script)
"sd", // Sindhi
// Less common RTL languages
"yi", // Yiddish
"ha", // Hausa (when written in Arabic script)
"ks", // Kashmiri (written in the Perso-Arabic script)
"bal", // Balochi (when written in Arabic script)
"khw", // Khowar (Chitrali)
"brh", // Brahui (when written in Arabic script)
"tmh", // Tamashek (some dialects use Arabic script)
"bgn", // Western Balochi
"arc", // Aramaic
"sam", // Samaritan Aramaic
"prd", // Parsi-Dari (a dialect of Persian)
"huz", // Hazaragi (a dialect of Persian)
"gbz", // Zaza (written in Arabic script in some areas)
"urj" // Urdu in Romanized script (not always RTL, but to account for edge cases)
/* spell-checker: enable */
].includes(currentLanguageTag);
})();
html.dir = isRtl ? "rtl" : "ltr";
}
const getLanguageLabel = (languageTag: LanguageTag) => {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
export * from "./getUserProfileApi";

View File

@ -0,0 +1,109 @@
import { assert } from "keycloakify/tools/assert";
let cleanup: (() => void) | undefined;
const handledElements = new WeakSet<HTMLElement>();
const KC_NUMBER_UNFORMAT = "kcNumberUnFormat";
const SELECTOR = `input[data-${KC_NUMBER_UNFORMAT}]`;
export function unFormatNumberOnSubmit() {
cleanup?.();
const handleElement = (element: HTMLInputElement) => {
if (handledElements.has(element)) {
return;
}
const form = element.closest("form");
if (form === null) {
return;
}
form.addEventListener("submit", () => {
const rawFormat = element.getAttribute(`data-${KC_NUMBER_UNFORMAT}`);
if (rawFormat) {
element.value = formatNumber(element.value, rawFormat);
}
});
handledElements.add(element);
};
document.querySelectorAll(SELECTOR).forEach(element => {
assert(element instanceof HTMLInputElement);
handleElement(element);
});
const observer = new MutationObserver(mutationsList => {
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = (node as HTMLElement).querySelector(SELECTOR);
if (element !== null) {
assert(element instanceof HTMLInputElement);
handleElement(element);
}
}
});
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
cleanup = () => observer.disconnect();
}
// NOTE: Keycloak code
const formatNumber = (input: string, format: string) => {
if (!input) {
return "";
}
// array holding the patterns for the number of expected digits in each part
const digitPattern = format.match(/{\d+}/g);
if (!digitPattern) {
return "";
}
// calculate the maximum size of the given pattern based on the sum of the expected digits
const maxSize = digitPattern.reduce(
(total, p) => total + parseInt(p.replace("{", "").replace("}", "")),
0
);
// keep only digits
let rawValue = input.replace(/\D+/g, "");
// make sure the value is a number
//@ts-expect-error
if (parseInt(rawValue) != rawValue) {
return "";
}
// make sure the number of digits does not exceed the maximum size
if (rawValue.length > maxSize) {
rawValue = rawValue.substring(0, maxSize);
}
// build the regex based based on the expected digits in each part
const formatter = digitPattern.reduce((result, p) => result + `(\\d${p})`, "^");
// if the current digits match the pattern we have each group of digits in an array
let digits = new RegExp(formatter).exec(rawValue);
// no match, return the raw value without any format
if (!digits) {
return input;
}
let result = format;
// finally format the current digits accordingly to the given format
for (let i = 0; i < digitPattern.length; i++) {
result = result.replace(digitPattern[i], digits[i + 1]);
}
return result;
};

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -30,7 +31,14 @@ export default function Code(props: PageProps<Extract<KcContext, { pageId: "code
<input id="code" className={kcClsx("kcTextareaClass")} defaultValue={code.code} />
</>
) : (
<p id="error">{code.error}</p>
code.error && (
<p
id="error"
dangerouslySetInnerHTML={{
__html: kcSanitize(code.error)
}}
/>
)
)}
</div>
</Template>

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -67,6 +68,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type I18nLike = {
msgStr: (key: "recovery-codes-download-file-header" | "recovery-codes-download-file-description" | "recovery-codes-download-file-date") => string;
@ -137,6 +138,12 @@ export function useScript(params: { olRecoveryCodesListId: string; i18n: I18nLik
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: olRecoveryCodesListId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -59,6 +60,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -88,6 +89,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -0,0 +1,30 @@
export async function waitForElementMountedOnDom(params: {
elementId: string;
}): Promise<void> {
const { elementId } = params;
const getElement = () => document.getElementById(elementId);
const element = getElement();
if (element === null) {
let prElementPresentInTheDom_resolve: () => void;
const prElementPresentInTheDom = new Promise<void>(
resolve => (prElementPresentInTheDom_resolve = resolve)
);
// Observe the dom for the element to be added
const observer = new MutationObserver(() => {
const element = getElement();
if (element === null) {
return;
}
observer.disconnect();
prElementPresentInTheDom_resolve();
});
observer.observe(document.body, { childList: true, subtree: true });
await prElementPresentInTheDom;
}
}

View File

@ -6,7 +6,7 @@ import {
} from "../bin/shared/constants";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
import { copyKeycloakResourcesToPublic } from "../bin/shared/copyKeycloakResourcesToPublic";
import { command as copyKeycloakResourcesToPublicCommand } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
import {
getBuildContext,
@ -15,7 +15,7 @@ import {
type ResolvedViteConfig
} from "../bin/shared/buildContext";
import MagicString from "magic-string";
import { generateKcGenTs } from "../bin/shared/generateKcGenTs";
import { command as updateKcGenCommand } from "../bin/update-kc-gen";
export namespace keycloakify {
export type Params = BuildOptions & {
@ -122,13 +122,12 @@ export function keycloakify(params: keycloakify.Params) {
}
const buildContext = getBuildContext({
cliCommandOptions: {
projectDirPath
}
projectDirPath
});
copyKeycloakResourcesToPublic({ buildContext }),
await generateKcGenTs({ buildContext });
await copyKeycloakResourcesToPublicCommand({ buildContext });
await updateKcGenCommand({ buildContext });
},
transform: (code, id) => {
assert(command !== undefined);

View File

@ -16,3 +16,42 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithErrorCode: Story = {
render: () => (
<KcPageStory
kcContext={{
code: {
success: false,
error: "Failed to generate code"
}
}}
/>
)
};
export const WithFrenchLanguage: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: {
currentLanguageTag: "fr"
},
code: {
success: true,
code: "XYZ789"
}
}}
/>
)
};
export const WithHtmlErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
code: {
success: false,
error: "Something went wrong. <a href='https://example.com'>Try again</a>"
}
}}
/>
)
};

View File

@ -16,3 +16,33 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithAIAFlow: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: true,
url: { loginAction: "/login-action" }
}}
/>
)
};
export const WithoutAIAFlow: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: false,
url: { loginAction: "/login-action" }
}}
/>
)
};
export const WithCustomButtonStyle: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: true,
url: { loginAction: "/login-action" }
}}
/>
)
};

View File

@ -16,3 +16,13 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithCustomCredentialLabel: Story = {
render: () => (
<KcPageStory
kcContext={{
credentialLabel: "Test Credential",
url: { loginAction: "/login-action" }
}}
/>
)
};

View File

@ -26,3 +26,38 @@ export const WithAnotherMessage: Story = {
/>
)
};
export const WithHtmlErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: {
summary: "<strong>Error:</strong> Something went wrong. <a href='https://example.com'>Go back</a>"
}
}}
/>
)
};
export const FrenchError: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: { currentLanguageTag: "fr" },
message: { summary: "Une erreur s'est produite" }
}}
/>
)
};
export const WithSkipLink: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "An error occurred" },
skipLink: true,
client: {
baseUrl: "https://example.com"
}
}}
/>
)
};

View File

@ -16,3 +16,14 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithoutRedirectUrl: Story = {
render: () => (
<KcPageStory
kcContext={{
logout: {
clients: []
}
}}
/>
)
};

View File

@ -16,3 +16,47 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithFormValidationErrors: Story = {
render: () => (
<KcPageStory
kcContext={{
messagesPerField: {
existsError: (fieldName: string) => ["email", "firstName"].includes(fieldName),
get: (fieldName: string) => {
if (fieldName === "email") return "Invalid email format.";
if (fieldName === "firstName") return "First name is required.";
}
}
}}
/>
)
};
export const WithReadOnlyFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
email: { value: "jane.doe@example.com", readOnly: true },
firstName: { value: "Jane", readOnly: false }
}
}
}}
/>
)
};
export const WithPrefilledFormFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
firstName: { value: "Jane" },
lastName: { value: "Doe" },
email: { value: "jane.doe@example.com" }
}
}
}}
/>
)
};

View File

@ -55,3 +55,42 @@ export const WithRequiredActions: Story = {
/>
)
};
export const WithPageRedirect: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "You will be redirected shortly." },
pageRedirectUri: "https://example.com"
}}
/>
)
};
export const WithoutClientBaseUrl: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "No client base URL defined." },
client: { baseUrl: undefined }
}}
/>
)
};
export const WithMessageHeader: Story = {
render: () => (
<KcPageStory
kcContext={{
messageHeader: "Important Notice",
message: { summary: "This is an important message." }
}}
/>
)
};
export const WithAdvancedMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "Please take note of this <strong>important</strong> information." }
}}
/>
)
};

View File

@ -231,3 +231,131 @@ export const WithErrorMessage: Story = {
/>
)
};
export const WithOneSocialProvider: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
}
]
}
}}
/>
)
};
export const WithTwoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
},
{
loginUrl: "microsoft",
alias: "microsoft",
providerId: "microsoft",
displayName: "Microsoft",
iconClasses: "fa fa-windows"
}
]
}
}}
/>
)
};
export const WithNoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: []
}
}}
/>
)
};
export const WithMoreThanTwoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
},
{
loginUrl: "microsoft",
alias: "microsoft",
providerId: "microsoft",
displayName: "Microsoft",
iconClasses: "fa fa-windows"
},
{
loginUrl: "facebook",
alias: "facebook",
providerId: "facebook",
displayName: "Facebook",
iconClasses: "fa fa-facebook"
},
{
loginUrl: "twitter",
alias: "twitter",
providerId: "twitter",
displayName: "Twitter",
iconClasses: "fa fa-twitter"
}
]
}
}}
/>
)
};
export const WithSocialProvidersAndWithoutRememberMe: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
}
]
},
realm: { rememberMe: false }
}}
/>
)
};

View File

@ -41,3 +41,24 @@ export const WithError: Story = {
/>
)
};
export const WithAppInitiatedAction: Story = {
render: () => (
<KcPageStory
kcContext={{
isAppInitiatedAction: true
}}
/>
)
};
export const WithPreFilledUserLabel: Story = {
render: () => (
<KcPageStory
kcContext={{
totp: {
otpCredentials: [{ userLabel: "MyDevice" }]
}
}}
/>
)
};

View File

@ -16,3 +16,49 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithIdpAlias: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: "Google",
brokerContext: {
username: "john.doe"
},
realm: {
displayName: "MyRealm"
}
}}
/>
)
};
export const WithoutIdpAlias: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: undefined,
brokerContext: {
username: "john.doe"
},
realm: {
displayName: "MyRealm"
}
}}
/>
)
};
export const WithCustomRealmDisplayName: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: "Facebook",
brokerContext: {
username: "jane.doe"
},
realm: {
displayName: "CustomRealm"
}
}}
/>
)
};

View File

@ -115,7 +115,6 @@ export const WithFavoritePet: Story = {
)
};
export const WithNewsletter: Story = {
render: () => (
<KcPageStory
@ -132,7 +131,7 @@ export const WithNewsletter: Story = {
},
annotations: {
inputOptionLabels: {
"yes": "I want my email inbox filled with spam"
yes: "I want my email inbox filled with spam"
},
inputType: "multiselect-checkboxes"
},
@ -140,13 +139,12 @@ export const WithNewsletter: Story = {
readOnly: false
} satisfies Attribute
}
},
}
}}
/>
)
};
export const WithEmailAsUsername: Story = {
render: () => (
<KcPageStory
@ -217,3 +215,65 @@ export const WithTermsAcceptance: Story = {
/>
)
};
export const WithTermsNotAccepted: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
termsAcceptanceRequired: true,
messagesPerField: {
existsError: (fieldName: string) => fieldName === "termsAccepted",
get: (fieldName: string) => (fieldName === "termsAccepted" ? "You must accept the terms." : undefined)
}
}}
/>
)
};
export const WithFieldErrors: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "" },
email: { value: "invalid-email" }
}
},
messagesPerField: {
existsError: (fieldName: string) => ["username", "email"].includes(fieldName),
get: (fieldName: string) => {
if (fieldName === "username") return "Username is required.";
if (fieldName === "email") return "Invalid email format.";
}
}
}}
/>
)
};
export const WithReadOnlyFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "johndoe", readOnly: true },
email: { value: "jhon.doe@gmail.com", readOnly: false }
}
}
}}
/>
)
};
export const WithAutoGeneratedUsername: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "autogenerated_username" }
}
}
}}
/>
)
};

View File

@ -45,3 +45,32 @@ export const French: Story = {
/>
)
};
export const WithErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
messagesPerField: {
existsError: () => true,
get: () => "An error occurred while processing your request."
}
}}
/>
)
};
export const Spanish: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: {
currentLanguageTag: "es"
},
"x-keycloakify": {
messages: {
termsText: "<p>Mis términos en <strong>Español</strong></p>"
}
}
}}
/>
)
};

View File

@ -2545,14 +2545,6 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@keycloakify/angular@^0.0.1-rc.19":
version "0.0.1-rc.19"
resolved "https://registry.yarnpkg.com/@keycloakify/angular/-/angular-0.0.1-rc.19.tgz#56edbfc74dc66d6ea23ff3b0d2e5ce48fad200c6"
integrity sha512-C2etRoAv2jCv+BMaIaJcEBKWEV8uM47pUpO37ZRFj78jChM+o8a0/Q+ooadkidKupvOe9K5SB91fdL3aUwTyEw==
dependencies:
tsafe "^1.7.5"
tslib "^2.3.0"
"@mdx-js/mdx@^1.6.22":
version "1.6.22"
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
@ -12614,11 +12606,6 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tslib@^2.3.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
tss-react@^4.9.10:
version "4.9.10"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.9.10.tgz#287392f4459116ac83c51fc117fad138ac06f139"