Compare commits
137 Commits
v9.4.0-rc.
...
v9.7.4
Author | SHA1 | Date | |
---|---|---|---|
f7bd16cb79 | |||
862ee31f1b | |||
66ead628f3 | |||
e143257b48 | |||
0b68e051e6 | |||
9557145f72 | |||
249877b9c5 | |||
ff2321fde5 | |||
1edd6e4193 | |||
bdde9162d9 | |||
99b4933536 | |||
c5caf7e0da | |||
bcc5308cfb | |||
9fb902db5c | |||
239f98aa9c | |||
f5d0511662 | |||
75582d2a26 | |||
0796b3dedf | |||
662a76bbb6 | |||
a664195625 | |||
e533e127bf | |||
346e3df009 | |||
19ba0873f5 | |||
fb4acc62c4 | |||
fd538e95ca | |||
def2d8b75b | |||
586b28af1c | |||
585c279d10 | |||
51bc65e671 | |||
ff1758cdce | |||
72a3c37e84 | |||
c99cdf5566 | |||
ad339710f1 | |||
00d2d12056 | |||
6a7b472c0e | |||
6991d868be | |||
85cc665d17 | |||
5bb22fc345 | |||
5417dc1bed | |||
ee20d33724 | |||
7887bd2b67 | |||
168582efea | |||
2c55d13f91 | |||
23e5f553d4 | |||
bc44eadcec | |||
a3e3136600 | |||
c7bfcee8d2 | |||
12ebd19716 | |||
aec9ffa5db | |||
2e6321342e | |||
6e71da62f0 | |||
5bf33aae75 | |||
06b2dc63ff | |||
1bb0c9dfc2 | |||
a2b167e120 | |||
0909a4b7cc | |||
fd7d2bb9bf | |||
63c40fd816 | |||
0569fa5e58 | |||
ba74952e0b | |||
20c28f785a | |||
e9b249ddc7 | |||
604bb484a3 | |||
010c93793a | |||
dc1d4a66f4 | |||
8ef633d7ef | |||
2176d33da1 | |||
5b794e2d22 | |||
ccd75d56c5 | |||
b700066833 | |||
546ee006d3 | |||
7f333a6a36 | |||
ae757ee371 | |||
69936750d5 | |||
442bfa4ed6 | |||
79e25e69bb | |||
b95c12772d | |||
de47525d7c | |||
f49d20e47c | |||
33b9917229 | |||
2a88e6802f | |||
bcc8b12e13 | |||
9b974505eb | |||
29b1c26771 | |||
02db20d98b | |||
757354df7d | |||
319d7dbe94 | |||
feb8eaf95a | |||
563518cf46 | |||
7c42d9082a | |||
040284af71 | |||
34f64184d9 | |||
b9abd74156 | |||
a1c0bfda6c | |||
617dcef09d | |||
d9c406800a | |||
54b869def1 | |||
d80a583979 | |||
99bfd7379b | |||
5f257382fa | |||
e3e6847c82 | |||
4ee0823acb | |||
d466123b1c | |||
21cbc14a48 | |||
b2f2c3e386 | |||
b03340ed10 | |||
5b563d8e9b | |||
2790487fc7 | |||
ad5a368065 | |||
7c0a631a9a | |||
4a8920749a | |||
8ab118dd06 | |||
e6661cb898 | |||
1671850714 | |||
d568bafe04 | |||
43dcce8478 | |||
ad70a4cffd | |||
6d4a948dd8 | |||
839ba6a964 | |||
b5cfdb9d0a | |||
9706338182 | |||
05f52c3d23 | |||
df3acb6932 | |||
b3c242595e | |||
26985f8d81 | |||
05e5e4efec | |||
e88be30fc8 | |||
4d67f16e94 | |||
334ec1870a | |||
ef5e4fccd3 | |||
8535edcfd4 | |||
bda76200d7 | |||
db0dc96cc7 | |||
6d62b5a150 | |||
217439d673 | |||
1f79a8f7dc | |||
22496e36eb |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -3,9 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v5
|
||||
- v6
|
||||
- v7
|
||||
- v9
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
42
README.md
42
README.md
@ -14,15 +14,15 @@
|
||||
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||
</a>
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/kYFZG7fQmn">
|
||||
<img src="https://img.shields.io/discord/1097708346976505977"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
Check out our discord server!<br/>
|
||||
<a href="https://discord.gg/mJdYJSdcm4">
|
||||
<img src="https://dcbadge.limes.pink/api/server/kYFZG7fQmn"/>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.keycloakify.dev">Home</a>
|
||||
-
|
||||
@ -41,13 +41,15 @@
|
||||
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
|
||||
</p>
|
||||
|
||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
|
||||
> WARNING: Do not use Keycloakify v9 if you are staring out!
|
||||
> Use the latest release candidate of Keycloakify v10 instead.
|
||||
> `yarn add keycloakify@next` See [npm](https://www.npmjs.com/package/keycloakify?activeTab=versions).
|
||||
|
||||
> 📣 I've observed that a few people have unstarred the project recently.
|
||||
> I'm concerned that I may have inadvertently introduced some misinformation in the documentation, leading to frustration.
|
||||
> If you're having a negative experience, [please let me know so I can resolve the issue](https://github.com/keycloakify/keycloakify/discussions/507).
|
||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
|
||||
## Sponsor 👼
|
||||
> NOTE: Keycloakify 10 is still in realase-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
||||
|
||||
## Sponsor
|
||||
|
||||
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
|
||||
Their dedicated support helps us continue the development and maintenance of this project.
|
||||
@ -70,12 +72,12 @@ Their dedicated support helps us continue the development and maintenance of thi
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud IAM</a> and use promo code <code>keycloakify5</code></i>
|
||||
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud-IAM</a> and use promo code <code>keycloakify5</code></i>
|
||||
<br/>
|
||||
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
|
||||
</p>
|
||||
|
||||
Thank you, [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), for your support!
|
||||
Thank you, [Cloud-IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), for your support!
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
@ -130,6 +132,20 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
|
||||
# Changelog highlights
|
||||
|
||||
## 9.5
|
||||
|
||||
- Post build hook: You can now apply custom transformation to your theme files. [Learn more](https://docs.keycloakify.dev/build-options#postbuild-hook).
|
||||
- You can now specify your option in the Keycloakify's Vite plugin instead in the package.json. [See example](https://docs.keycloakify.dev/build-options#themename).
|
||||
|
||||
## 9.4
|
||||
|
||||
**Vite Support! 🎉**
|
||||
|
||||
- [The starter is now a Vite project](https://github.com/keycloakify/keycloakify-starter).
|
||||
The Webpack based starter is accessible [here](https://github.com/keycloakify/keycloakify-starter-cra).
|
||||
- CRA (Webpack) remains supported for the forseable future.
|
||||
- If you have a CRA Keycloakify theme that you wish to migrate to Vite checkout [this migration guide](https://docs.keycloakify.dev/migration-guides/cra-greater-than-vite).
|
||||
|
||||
## 9.0
|
||||
|
||||
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "9.4.0-rc.4",
|
||||
"version": "9.7.4",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -113,19 +113,19 @@
|
||||
"@babel/parser": "^7.22.7",
|
||||
"@babel/types": "^7.22.5",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"cheerio": "1.0.0-rc.5",
|
||||
"cli-select": "^1.1.2",
|
||||
"evt": "^2.4.18",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"minimal-polyfills": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react-markdown": "^5.0.3",
|
||||
"recast": "^0.23.3",
|
||||
"rfc4648": "^1.5.2",
|
||||
"tsafe": "^1.6.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1",
|
||||
"zod": "^3.17.10"
|
||||
"zod": "^3.17.10",
|
||||
"magic-string": "^0.30.7"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
|
||||
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { getLogger } from "../src/bin/tools/logger";
|
||||
|
||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
@ -19,7 +19,9 @@ const logger = getLogger({ isSilent });
|
||||
async function main() {
|
||||
const keycloakVersion = "23.0.4";
|
||||
|
||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
|
||||
|
||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
@ -31,7 +33,8 @@ async function main() {
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
"buildOptions": {
|
||||
"cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
|
||||
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
|
||||
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,14 +75,13 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
|
||||
const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
|
||||
|
||||
const languages = Object.keys(recordForPageType);
|
||||
|
||||
const generatedFileHeader = [
|
||||
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
|
||||
"//PLEASE DO NOT EDIT MANUALLY",
|
||||
""
|
||||
`//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
|
||||
"//PLEASE DO NOT EDIT MANUALLY"
|
||||
].join("\n");
|
||||
|
||||
languages.forEach(language => {
|
||||
@ -92,6 +94,7 @@ async function main() {
|
||||
Buffer.from(
|
||||
[
|
||||
generatedFileHeader,
|
||||
"",
|
||||
"/* spell-checker: disable */",
|
||||
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||
"",
|
||||
@ -110,10 +113,15 @@ async function main() {
|
||||
Buffer.from(
|
||||
[
|
||||
generatedFileHeader,
|
||||
`import * as en from "./en";`,
|
||||
"",
|
||||
"export async function getMessages(currentLanguageTag: string) {",
|
||||
" const { default: messages } = await (() => {",
|
||||
" switch (currentLanguageTag) {",
|
||||
...languages.map(language => ` case "${language}": return import("./${language}");`),
|
||||
` case "en": return en;`,
|
||||
...languages
|
||||
.filter(language => language !== "en")
|
||||
.map(language => ` case "${language}": return import("./${language}");`),
|
||||
' default: return { "default": {} };',
|
||||
" }",
|
||||
" })();",
|
||||
|
@ -1,14 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
|
||||
|
||||
const jsonSchemaName = "keycloakifyPackageJsonSchema";
|
||||
const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
|
||||
|
||||
const baseProperties = {
|
||||
// merges package.json schema with keycloakify properties
|
||||
"allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));
|
@ -1,11 +1,11 @@
|
||||
import { execSync } from "child_process";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
|
||||
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||
|
||||
const rootDirPath = getProjectRoot();
|
||||
const rootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||
fs.writeFileSync(
|
||||
|
21
src/PUBLIC_URL.ts
Normal file
21
src/PUBLIC_URL.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
* This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
|
||||
* This works both in your main app and in your Keycloak theme.
|
||||
*/
|
||||
export const PUBLIC_URL = (() => {
|
||||
const kcContext = (window as any)[nameOfTheGlobal];
|
||||
|
||||
if (kcContext === undefined || process.env.NODE_ENV === "development") {
|
||||
assert(
|
||||
process.env.PUBLIC_URL !== undefined,
|
||||
`If you use keycloakify/PUBLIC_URL you should be in Webpack and thus process.env.PUBLIC_URL should be defined`
|
||||
);
|
||||
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
|
||||
return `${kcContext.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}`;
|
||||
})();
|
@ -3,9 +3,14 @@ import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { I18n } from "keycloakify/account/i18n";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import FederatedIdentity from "./pages/FederatedIdentity";
|
||||
|
||||
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||
const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
|
||||
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
|
||||
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
|
||||
const Log = lazy(() => import("keycloakify/account/pages/Log"));
|
||||
|
||||
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||
const { kcContext, ...rest } = props;
|
||||
@ -16,8 +21,18 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||
switch (kcContext.pageId) {
|
||||
case "password.ftl":
|
||||
return <Password kcContext={kcContext} {...rest} />;
|
||||
case "sessions.ftl":
|
||||
return <Sessions kcContext={kcContext} {...rest} />;
|
||||
case "account.ftl":
|
||||
return <Account kcContext={kcContext} {...rest} />;
|
||||
case "totp.ftl":
|
||||
return <Totp kcContext={kcContext} {...rest} />;
|
||||
case "applications.ftl":
|
||||
return <Applications kcContext={kcContext} {...rest} />;
|
||||
case "log.ftl":
|
||||
return <Log kcContext={kcContext} {...rest} />;
|
||||
case "federatedIdentity.ftl":
|
||||
return <FederatedIdentity kcContext={kcContext} {...rest} />;
|
||||
}
|
||||
assert<Equals<typeof kcContext, never>>(false);
|
||||
})()}
|
||||
|
@ -23,7 +23,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("accountManagementTitle")
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
|
@ -11,4 +11,17 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||
export type ClassKey =
|
||||
| "kcHtmlClass"
|
||||
| "kcBodyClass"
|
||||
| "kcButtonClass"
|
||||
| "kcButtonPrimaryClass"
|
||||
| "kcButtonLargeClass"
|
||||
| "kcButtonDefaultClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcFormClass"
|
||||
| "kcFormGroupClass"
|
||||
| "kcInputWrapperClass"
|
||||
| "kcLabelClass"
|
||||
| "kcInputClass"
|
||||
| "kcInputErrorMessageClass";
|
||||
|
@ -3,7 +3,14 @@ import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import { type ThemeType } from "keycloakify/bin/constants";
|
||||
|
||||
export type KcContext = KcContext.Password | KcContext.Account;
|
||||
export type KcContext =
|
||||
| KcContext.Password
|
||||
| KcContext.Account
|
||||
| KcContext.Sessions
|
||||
| KcContext.Totp
|
||||
| KcContext.Applications
|
||||
| KcContext.Log
|
||||
| KcContext.FederatedIdentity;
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
@ -27,6 +34,7 @@ export declare namespace KcContext {
|
||||
sessionsUrl: string;
|
||||
applicationsUrl: string;
|
||||
logUrl: string;
|
||||
logoutUrl: string;
|
||||
resourceUrl: string;
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
@ -90,6 +98,7 @@ export declare namespace KcContext {
|
||||
lastName?: string;
|
||||
username?: string;
|
||||
};
|
||||
properties: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
export type Password = Common & {
|
||||
@ -111,6 +120,171 @@ export declare namespace KcContext {
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Sessions = Common & {
|
||||
pageId: "sessions.ftl";
|
||||
sessions: {
|
||||
sessions: {
|
||||
expires: string;
|
||||
clients: string[];
|
||||
ipAddress: string;
|
||||
started: string;
|
||||
lastAccess: string;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Totp = Common & {
|
||||
pageId: "totp.ftl";
|
||||
totp: {
|
||||
enabled: boolean;
|
||||
totpSecretEncoded: string;
|
||||
qrUrl: string;
|
||||
policy: {
|
||||
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
|
||||
digits: number;
|
||||
lookAheadWindow: number;
|
||||
} & (
|
||||
| {
|
||||
type: "totp";
|
||||
period: number;
|
||||
}
|
||||
| {
|
||||
type: "hotp";
|
||||
initialCounter: number;
|
||||
}
|
||||
);
|
||||
supportedApplications: string[];
|
||||
totpSecretQrCode: string;
|
||||
manualUrl: string;
|
||||
totpSecret: string;
|
||||
otpCredentials: { id: string; userLabel: string }[];
|
||||
};
|
||||
mode?: "qr" | "manual" | undefined | null;
|
||||
isAppInitiatedAction: boolean;
|
||||
url: {
|
||||
accountUrl: string;
|
||||
passwordUrl: string;
|
||||
totpUrl: string;
|
||||
socialUrl: string;
|
||||
sessionsUrl: string;
|
||||
applicationsUrl: string;
|
||||
logUrl: string;
|
||||
resourceUrl: string;
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
|
||||
referrerURI?: string;
|
||||
getLogoutUrl: () => string;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Applications = Common & {
|
||||
pageId: "applications.ftl";
|
||||
features: {
|
||||
log: boolean;
|
||||
identityFederation: boolean;
|
||||
authorization: boolean;
|
||||
passwordUpdateSupported: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
applications: {
|
||||
applications: {
|
||||
realmRolesAvailable: {
|
||||
name: string;
|
||||
description: string;
|
||||
compositesStream?: Record<string, unknown>;
|
||||
clientRole?: boolean;
|
||||
composite?: boolean;
|
||||
id?: string;
|
||||
containerId?: string;
|
||||
attributes?: Record<string, unknown>;
|
||||
}[];
|
||||
resourceRolesAvailable: Record<
|
||||
string,
|
||||
{
|
||||
roleName: string;
|
||||
roleDescription?: string;
|
||||
clientName: string;
|
||||
clientId: string;
|
||||
}[]
|
||||
>;
|
||||
additionalGrants: string[];
|
||||
clientScopesGranted: string[];
|
||||
effectiveUrl?: string;
|
||||
client: {
|
||||
alwaysDisplayInConsole: boolean;
|
||||
attributes: Record<string, unknown>;
|
||||
authenticationFlowBindingOverrides: Record<string, unknown>;
|
||||
baseUrl?: string;
|
||||
bearerOnly: boolean;
|
||||
clientAuthenticatorType: string;
|
||||
clientId: string;
|
||||
consentRequired: boolean;
|
||||
consentScreenText: string;
|
||||
description: string;
|
||||
directAccessGrantsEnabled: boolean;
|
||||
displayOnConsentScreen: boolean;
|
||||
dynamicScope: boolean;
|
||||
enabled: boolean;
|
||||
frontchannelLogout: boolean;
|
||||
fullScopeAllowed: boolean;
|
||||
id: string;
|
||||
implicitFlowEnabled: boolean;
|
||||
includeInTokenScope: boolean;
|
||||
managementUrl: string;
|
||||
name?: string;
|
||||
nodeReRegistrationTimeout: string;
|
||||
notBefore: string;
|
||||
protocol: string;
|
||||
protocolMappersStream: Record<string, unknown>;
|
||||
publicClient: boolean;
|
||||
realm: Record<string, unknown>;
|
||||
realmScopeMappingsStream: Record<string, unknown>;
|
||||
redirectUris: string[];
|
||||
registeredNodes: Record<string, unknown>;
|
||||
rolesStream: Record<string, unknown>;
|
||||
rootUrl?: string;
|
||||
scopeMappingsStream: Record<string, unknown>;
|
||||
secret: string;
|
||||
serviceAccountsEnabled: boolean;
|
||||
standardFlowEnabled: boolean;
|
||||
surrogateAuthRequired: boolean;
|
||||
webOrigins: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type Log = Common & {
|
||||
pageId: "log.ftl";
|
||||
log: {
|
||||
events: {
|
||||
date: string | number | Date;
|
||||
event: string;
|
||||
ipAddress: string;
|
||||
client: string;
|
||||
details: { value: string; key: string }[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type FederatedIdentity = Common & {
|
||||
pageId: "federatedIdentity.ftl";
|
||||
stateChecker: string;
|
||||
federatedIdentity: {
|
||||
identities: {
|
||||
providerId: string;
|
||||
displayName: string;
|
||||
userName: string;
|
||||
connected: boolean;
|
||||
}[];
|
||||
removeLinkPossible: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -1,16 +1,16 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { isStorybook } from "keycloakify/lib/isStorybook";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { resources_common } from "keycloakify/bin/constants";
|
||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||
|
||||
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
mockProperties?: Record<string, string>;
|
||||
}) {
|
||||
const { mockData } = params ?? {};
|
||||
const { mockData, mockProperties } = params ?? {};
|
||||
|
||||
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||
mockPageId?: PageId;
|
||||
@ -27,7 +27,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
warn_that_mock_is_enbaled: {
|
||||
if (isStorybook) {
|
||||
break warn_that_mock_is_enbaled;
|
||||
}
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
}
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
@ -77,6 +83,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
});
|
||||
}
|
||||
|
||||
if (mockProperties !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext.properties,
|
||||
"source": mockProperties
|
||||
});
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
@ -88,8 +101,6 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
|
||||
|
||||
return { "kcContext": realKcContext as any };
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
|
||||
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
|
||||
|
||||
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
|
||||
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
@ -15,10 +13,11 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeName": "my-theme-name",
|
||||
"url": {
|
||||
resourcesPath,
|
||||
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
|
||||
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
|
||||
"resourceUrl": "#",
|
||||
"accountUrl": "#",
|
||||
"applicationsUrl": "#",
|
||||
"logoutUrl": "#",
|
||||
"getLogoutUrl": () => "#",
|
||||
"logUrl": "#",
|
||||
"passwordUrl": "#",
|
||||
@ -147,6 +146,17 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
"lastName": "doe",
|
||||
"email": "john.doe@code.gouv.fr",
|
||||
"username": "doe_j"
|
||||
},
|
||||
"properties": {
|
||||
"parent": "account-v1",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"accountResourceProvider": "account-v1",
|
||||
"styles":
|
||||
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
"kcButtonClass": "btn",
|
||||
"kcButtonDefaultClass": "btn-default"
|
||||
}
|
||||
};
|
||||
|
||||
@ -173,5 +183,78 @@ export const kcContextMocks: KcContext[] = [
|
||||
"editUsernameAllowed": true
|
||||
},
|
||||
"stateChecker": ""
|
||||
}),
|
||||
id<KcContext.Sessions>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "sessions.ftl",
|
||||
"sessions": {
|
||||
"sessions": [
|
||||
{
|
||||
"ipAddress": "127.0.0.1",
|
||||
"started": new Date().toString(),
|
||||
"lastAccess": new Date().toString(),
|
||||
"expires": new Date().toString(),
|
||||
"clients": ["Chrome", "Firefox"],
|
||||
"id": "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stateChecker": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
}),
|
||||
id<KcContext.Totp>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "totp.ftl",
|
||||
"totp": {
|
||||
"enabled": true,
|
||||
"totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
"qrUrl": "#",
|
||||
"totpSecretQrCode":
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
|
||||
"manualUrl": "#",
|
||||
"totpSecret": "G4nsI8lQagRMUchH8jEG",
|
||||
"otpCredentials": [],
|
||||
"supportedApplications": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
"policy": {
|
||||
"algorithm": "HmacSHA1",
|
||||
"digits": 6,
|
||||
"lookAheadWindow": 1,
|
||||
"type": "totp",
|
||||
"period": 30
|
||||
}
|
||||
},
|
||||
"mode": "qr",
|
||||
"isAppInitiatedAction": false,
|
||||
"stateChecker": ""
|
||||
}),
|
||||
id<KcContext.Log>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "log.ftl",
|
||||
"log": {
|
||||
"events": [
|
||||
{
|
||||
"date": "2/21/2024, 1:28:39 PM",
|
||||
"event": "login",
|
||||
"ipAddress": "172.17.0.1",
|
||||
"client": "security-admin-console",
|
||||
"details": [{ key: "openid-connect", value: "admin" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContext.FederatedIdentity>({
|
||||
...kcContextCommonMock,
|
||||
"stateChecker": "",
|
||||
"pageId": "federatedIdentity.ftl",
|
||||
"federatedIdentity": {
|
||||
"identities": [
|
||||
{
|
||||
"providerId": "keycloak-oidc",
|
||||
"displayName": "keycloak-oidc",
|
||||
"userName": "John",
|
||||
"connected": true
|
||||
}
|
||||
],
|
||||
"removeLinkPossible": true
|
||||
}
|
||||
})
|
||||
];
|
||||
|
@ -6,8 +6,15 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"kcHtmlClass": undefined,
|
||||
"kcBodyClass": undefined,
|
||||
"kcButtonClass": "btn",
|
||||
"kcContentWrapperClass": "row",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"kcButtonDefaultClass": "btn-default"
|
||||
"kcButtonDefaultClass": "btn-default",
|
||||
"kcFormClass": "form-horizontal",
|
||||
"kcFormGroupClass": "form-group",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcLabelClass": "control-label",
|
||||
"kcInputClass": "form-control",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
}
|
||||
});
|
||||
|
138
src/account/pages/Applications.tsx
Normal file
138
src/account/pages/Applications.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
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";
|
||||
|
||||
function isArrayWithEmptyObject(variable: any): boolean {
|
||||
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
||||
}
|
||||
|
||||
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const {
|
||||
url,
|
||||
applications: { applications },
|
||||
stateChecker
|
||||
} = kcContext;
|
||||
|
||||
const { msg, advancedMsg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="applications">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("applicationsHtmlTitle")}</h2>
|
||||
</div>
|
||||
|
||||
<form action={url.applicationsUrl} method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" id="referrer" name="referrer" value={stateChecker} />
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{msg("application")}</td>
|
||||
<td>{msg("availableRoles")}</td>
|
||||
<td>{msg("grantedPermissions")}</td>
|
||||
<td>{msg("additionalGrants")}</td>
|
||||
<td>{msg("action")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{applications.map(application => (
|
||||
<tr key={application.client.clientId}>
|
||||
<td>
|
||||
{application.effectiveUrl && (
|
||||
<a href={application.effectiveUrl}>
|
||||
{(application.client.name && advancedMsg(application.client.name)) || application.client.clientId}
|
||||
</a>
|
||||
)}
|
||||
{!application.effectiveUrl &&
|
||||
((application.client.name && advancedMsg(application.client.name)) || application.client.clientId)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) &&
|
||||
application.realmRolesAvailable.map((role, index) => (
|
||||
<span key={role.name}>
|
||||
{role.description ? advancedMsg(role.description) : advancedMsg(role.name)}
|
||||
{index < application.realmRolesAvailable.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}
|
||||
{application.resourceRolesAvailable &&
|
||||
Object.keys(application.resourceRolesAvailable).map(resource => (
|
||||
<span key={resource}>
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) && ", "}
|
||||
{application.resourceRolesAvailable[resource].map(clientRole => (
|
||||
<span key={clientRole.roleName}>
|
||||
{clientRole.roleDescription
|
||||
? advancedMsg(clientRole.roleDescription)
|
||||
: advancedMsg(clientRole.roleName)}{" "}
|
||||
{msg("inResource")}{" "}
|
||||
<strong>
|
||||
{clientRole.clientName ? advancedMsg(clientRole.clientName) : clientRole.clientId}
|
||||
</strong>
|
||||
{clientRole !==
|
||||
application.resourceRolesAvailable[resource][
|
||||
application.resourceRolesAvailable[resource].length - 1
|
||||
] && ", "}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{application.client.consentRequired ? (
|
||||
application.clientScopesGranted.map(claim => (
|
||||
<span key={claim}>
|
||||
{advancedMsg(claim)}
|
||||
{claim !== application.clientScopesGranted[application.clientScopesGranted.length - 1] && ", "}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<strong>{msg("fullAccess")}</strong>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{application.additionalGrants.map(grant => (
|
||||
<span key={grant}>
|
||||
{advancedMsg(grant)}
|
||||
{grant !== application.additionalGrants[application.additionalGrants.length - 1] && ", "}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{(application.client.consentRequired && application.clientScopesGranted.length > 0) ||
|
||||
application.additionalGrants.length > 0 ? (
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(getClassName("kcButtonPrimaryClass"), getClassName("kcButtonClass"))}
|
||||
id={`revoke-${application.client.clientId}`}
|
||||
name="clientId"
|
||||
value={application.client.id}
|
||||
>
|
||||
{msg("revoke")}
|
||||
</button>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
58
src/account/pages/FederatedIdentity.tsx
Normal file
58
src/account/pages/FederatedIdentity.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { PageProps } from "keycloakify/account";
|
||||
import { I18n } from "keycloakify/account/i18n";
|
||||
import { KcContext } from "keycloakify/account/kcContext";
|
||||
|
||||
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { url, federatedIdentity, stateChecker } = kcContext;
|
||||
const { msg } = i18n;
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity">
|
||||
<div className="main-layout social">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("federatedIdentitiesHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="federated-identities">
|
||||
{federatedIdentity.identities.map(identity => (
|
||||
<div key={identity.providerId} className="row margin-bottom">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor={identity.providerId} className="control-label">
|
||||
{identity.displayName}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-5">
|
||||
<input disabled className="form-control" value={identity.userName} />
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-5">
|
||||
{identity.connected ? (
|
||||
federatedIdentity.removeLinkPossible && (
|
||||
<form action={url.socialUrl} method="post" className="form-inline">
|
||||
<input type="hidden" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" name="action" value="remove" />
|
||||
<input type="hidden" name="providerId" value={identity.providerId} />
|
||||
<button id={`remove-link-${identity.providerId}`} className="btn btn-default">
|
||||
{msg("doRemove")}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
) : (
|
||||
<form action={url.socialUrl} method="post" className="form-inline">
|
||||
<input type="hidden" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" name="action" value="add" />
|
||||
<input type="hidden" name="providerId" value={identity.providerId} />
|
||||
<button id={`add-link-${identity.providerId}`} className="btn btn-default">
|
||||
{msg("doAdd")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
70
src/account/pages/Log.tsx
Normal file
70
src/account/pages/Log.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Key } from "react";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
|
||||
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { log } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("accountLogHtmlTitle")}</h2>
|
||||
</div>
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{msg("date")}</td>
|
||||
<td>{msg("event")}</td>
|
||||
<td>{msg("ip")}</td>
|
||||
<td>{msg("client")}</td>
|
||||
<td>{msg("details")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{log.events.map(
|
||||
(
|
||||
event: {
|
||||
date: string | number | Date;
|
||||
event: string;
|
||||
ipAddress: string;
|
||||
client: any;
|
||||
details: any[];
|
||||
},
|
||||
index: Key | null | undefined
|
||||
) => (
|
||||
<tr key={index}>
|
||||
<td>{event.date ? new Date(event.date).toLocaleString() : ""}</td>
|
||||
<td>{event.event}</td>
|
||||
<td>{event.ipAddress}</td>
|
||||
<td>{event.client || ""}</td>
|
||||
<td>
|
||||
{event.details.map((detail, detailIndex) => (
|
||||
<span key={detailIndex}>
|
||||
{`${detail.key} = ${detail.value}`}
|
||||
{detailIndex < event.details.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import type { I18n } from "keycloakify/account/i18n";
|
||||
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import type { KcContext } from "keycloakify/account/kcContext";
|
||||
|
||||
export type PageProps<KcContext, I18nExtended extends I18n> = {
|
||||
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
|
||||
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||
kcContext: KcContext;
|
||||
kcContext: NarowedKcContext;
|
||||
i18n: I18nExtended;
|
||||
doUseDefaultCss: boolean;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
|
65
src/account/pages/Sessions.tsx
Normal file
65
src/account/pages/Sessions.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
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 Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, stateChecker, sessions } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("sessionsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{msg("ip")}</th>
|
||||
<th>{msg("started")}</th>
|
||||
<th>{msg("lastAccess")}</th>
|
||||
<th>{msg("expires")}</th>
|
||||
<th>{msg("clients")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody role="rowgroup">
|
||||
{sessions.sessions.map((session, index: number) => (
|
||||
<tr key={index}>
|
||||
<td>{session.ipAddress}</td>
|
||||
<td>{session?.started}</td>
|
||||
<td>{session?.lastAccess}</td>
|
||||
<td>{session?.expires}</td>
|
||||
<td>
|
||||
{session.clients.map((client: string, clientIndex: number) => (
|
||||
<div key={clientIndex}>
|
||||
{client}
|
||||
<br />
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action={url.sessionsUrl} method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
|
||||
{msg("doLogOutAllSessions")}
|
||||
</button>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
235
src/account/pages/Totp.tsx
Normal file
235
src/account/pages/Totp.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
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";
|
||||
import { MessageKey } from "keycloakify/account/i18n/i18n";
|
||||
|
||||
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
|
||||
"HmacSHA1": "SHA1",
|
||||
"HmacSHA256": "SHA256",
|
||||
"HmacSHA512": "SHA512"
|
||||
};
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("authenticatorTitle")}</h2>
|
||||
</div>
|
||||
{totp.otpCredentials.length === 0 && (
|
||||
<div className="subtitle col-md-2">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{totp.enabled && (
|
||||
<table className="table table-bordered table-striped">
|
||||
<thead>
|
||||
{totp.otpCredentials.length > 1 ? (
|
||||
<tr>
|
||||
<th colSpan={4}>{msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th colSpan={3}>{msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{totp.otpCredentials.map((credential, index) => (
|
||||
<tr key={index}>
|
||||
<td className="provider">{msg("mobile")}</td>
|
||||
{totp.otpCredentials.length > 1 && <td className="provider">{credential.id}</td>}
|
||||
<td className="provider">{credential.userLabel || ""}</td>
|
||||
<td className="action">
|
||||
<form action={url.totpUrl} method="post" className="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" id="submitAction" name="submitAction" value="Delete" />
|
||||
<input type="hidden" id="credentialId" name="credentialId" value={credential.id} />
|
||||
<button id={`remove-mobile-${index}`} className="btn btn-default">
|
||||
<i className="pficon pficon-delete"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{!totp.enabled && (
|
||||
<div>
|
||||
<hr />
|
||||
<ol id="kc-totp-settings">
|
||||
<li>
|
||||
<p>{msg("totpStep1")}</p>
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.supportedApplications?.map(app => (
|
||||
<li key={app}>{msg(app as MessageKey)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{mode && mode == "manual" ? (
|
||||
<>
|
||||
<li>
|
||||
<p>{msg("totpManualStep2")}</p>
|
||||
<p>
|
||||
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href={totp.qrUrl} id="mode-barcode">
|
||||
{msg("totpScanBarcode")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>{msg("totpManualStep3")}</p>
|
||||
<ul>
|
||||
<li id="kc-totp-type">
|
||||
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
|
||||
</li>
|
||||
<li id="kc-totp-algorithm">
|
||||
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
|
||||
</li>
|
||||
<li id="kc-totp-digits">
|
||||
{msg("totpDigits")}: {totp.policy.digits}
|
||||
</li>
|
||||
{totp.policy.type === "totp" ? (
|
||||
<li id="kc-totp-period">
|
||||
{msg("totpInterval")}: {totp.policy.period}
|
||||
</li>
|
||||
) : (
|
||||
<li id="kc-totp-counter">
|
||||
{msg("totpCounter")}: {totp.policy.initialCounter}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li>
|
||||
<p>{msg("totpStep2")}</p>
|
||||
<p>
|
||||
<img
|
||||
id="kc-totp-secret-qr-code"
|
||||
src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
|
||||
alt="Figure: Barcode"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<a href={totp.manualUrl} id="mode-manual">
|
||||
{msg("totpUnableToScan")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<p>{msg("totpStep3")}</p>
|
||||
<p>{msg("totpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<form action={url.totpUrl} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="totp" className="control-label">
|
||||
{msg("authenticatorCode")}
|
||||
</label>
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
autoComplete="off"
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
|
||||
{msg("totpDeviceName")}
|
||||
</label>
|
||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="text"
|
||||
id="userLabel"
|
||||
name="userLabel"
|
||||
autoComplete="off"
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(getClassName("kcFormGroupClass"), "text-right")}>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSave")}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonDefaultClass"),
|
||||
getClassName("kcButtonLargeClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
id="cancelTOTPBtn"
|
||||
name="submitAction"
|
||||
value="Cancel"
|
||||
>
|
||||
{msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ export const nameOfTheGlobal = "kcContext";
|
||||
export const keycloak_resources = "keycloak-resources";
|
||||
export const resources_common = "resources-common";
|
||||
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
|
||||
export const resolvedViteConfigJsonBasename = ".keycloakifyViteConfig.json";
|
||||
export const resolvedViteConfigJsonBasename = "vite.json";
|
||||
export const basenameOfTheKeycloakifyResourcesDir = "build";
|
||||
|
||||
export const themeTypes = ["login", "account"] as const;
|
||||
@ -10,3 +10,5 @@ export const retrocompatPostfix = "_retrocompat";
|
||||
export const accountV1ThemeName = "account-v1";
|
||||
|
||||
export type ThemeType = (typeof themeTypes)[number];
|
||||
|
||||
export const keycloakifyBuildOptionsForPostPostBuildScriptEnvName = "KEYCLOAKIFY_BUILD_OPTIONS_POST_POST_BUILD_SCRIPT";
|
||||
|
@ -1,17 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
|
||||
import { downloadKeycloakStaticResources, type BuildOptionsLike } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { readBuildOptions } from "./keycloakify/buildOptions";
|
||||
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
|
||||
import { readThisNpmProjectVersion } from "./tools/readThisNpmProjectVersion";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "./tools/fs.rmSync";
|
||||
|
||||
(async () => {
|
||||
const buildOptions = readBuildOptions({
|
||||
"processArgv": process.argv.slice(2)
|
||||
export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
|
||||
const { processArgv } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ processArgv });
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
|
||||
|
||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||
|
||||
const { keycloakifyBuildinfoRaw } = generateKeycloakifyBuildinfoRaw({
|
||||
destDirPath,
|
||||
"keycloakifyVersion": readThisNpmProjectVersion(),
|
||||
buildOptions
|
||||
});
|
||||
|
||||
const reservedDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
|
||||
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 });
|
||||
|
||||
for (const themeType of themeTypes) {
|
||||
await downloadKeycloakStaticResources({
|
||||
@ -24,13 +51,13 @@ import * as fs from "fs";
|
||||
}
|
||||
})(),
|
||||
themeType,
|
||||
"themeDirPath": reservedDirPath,
|
||||
"themeDirPath": destDirPath,
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(reservedDirPath, "README.txt"),
|
||||
pathJoin(destDirPath, "README.txt"),
|
||||
Buffer.from(
|
||||
// prettier-ignore
|
||||
[
|
||||
@ -42,5 +69,44 @@ import * as fs from "fs";
|
||||
|
||||
fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
console.log(`${pathRelative(buildOptions.reactAppRootDirPath, reservedDirPath)} directory created.`);
|
||||
})();
|
||||
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
|
||||
}
|
||||
|
||||
export function generateKeycloakifyBuildinfoRaw(params: {
|
||||
destDirPath: string;
|
||||
keycloakifyVersion: string;
|
||||
buildOptions: BuildOptionsLike & {
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
};
|
||||
}) {
|
||||
const { destDirPath, keycloakifyVersion, buildOptions } = params;
|
||||
|
||||
const { cacheDirPath, npmWorkspaceRootDirPath, loginThemeResourcesFromKeycloakVersion, ...rest } = buildOptions;
|
||||
|
||||
assert<Equals<typeof rest, {}>>(true);
|
||||
|
||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||
{
|
||||
keycloakifyVersion,
|
||||
"buildOptions": {
|
||||
loginThemeResourcesFromKeycloakVersion,
|
||||
"cacheDirPath": pathRelative(destDirPath, cacheDirPath),
|
||||
"npmWorkspaceRootDirPath": pathRelative(destDirPath, npmWorkspaceRootDirPath)
|
||||
}
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
return { keycloakifyBuildinfoRaw };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await copyKeycloakResourcesToPublic({
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||
import { downloadAndUnzip } from "./downloadAndUnzip";
|
||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||
import { getLogger } from "./tools/logger";
|
||||
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
|
||||
@ -13,6 +13,7 @@ import { transformCodebase } from "./tools/transformCodebase";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
@ -21,11 +22,10 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
|
||||
const { keycloakVersion, destDirPath, buildOptions } = params;
|
||||
|
||||
await downloadAndUnzip({
|
||||
"doUseCache": true,
|
||||
"cacheDirPath": buildOptions.cacheDirPath,
|
||||
destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
||||
buildOptions,
|
||||
"preCacheTransform": {
|
||||
"actionCacheId": "npm install and build",
|
||||
"action": async ({ destDirPath }) => {
|
||||
@ -50,6 +50,62 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
|
||||
});
|
||||
}
|
||||
|
||||
repatriate_common_resources_from_base_login_theme: {
|
||||
const baseLoginThemeResourceDir = pathJoin(destDirPath, "base", "login", "resources");
|
||||
|
||||
if (!fs.existsSync(baseLoginThemeResourceDir)) {
|
||||
break repatriate_common_resources_from_base_login_theme;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": baseLoginThemeResourceDir,
|
||||
"destDirPath": pathJoin(destDirPath, "keycloak", "login", "resources")
|
||||
});
|
||||
}
|
||||
|
||||
install_and_move_to_common_resources_generated_in_keycloak_v2: {
|
||||
if (!fs.readFileSync(pathJoin(destDirPath, "keycloak", "login", "theme.properties")).toString("utf8").includes("web_modules")) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
|
||||
|
||||
if (!fs.existsSync(accountV2DirSrcDirPath)) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
||||
|
||||
if (packageManager === "pnpm") {
|
||||
try {
|
||||
child_process.execSync(`which pnpm`);
|
||||
} catch {
|
||||
console.log(`Installing pnpm globally`);
|
||||
child_process.execSync(`npm install -g pnpm`);
|
||||
}
|
||||
}
|
||||
|
||||
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
|
||||
|
||||
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
|
||||
|
||||
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
|
||||
|
||||
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
|
||||
.replace(`${packageManager} run check-types`, "true")
|
||||
.replace(`${packageManager} run babel`, "true");
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
|
||||
|
||||
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
|
||||
|
||||
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
|
||||
}
|
||||
|
||||
remove_keycloak_v2: {
|
||||
const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
|
||||
|
||||
@ -148,7 +204,7 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
|
||||
break remove_unused_lib;
|
||||
}
|
||||
|
||||
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace", "pficon"];
|
||||
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace"];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": libDirPath,
|
||||
@ -181,34 +237,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
|
||||
|
||||
fs.writeFileSync(
|
||||
totpFtlFilePath,
|
||||
Buffer.from(
|
||||
fs
|
||||
.readFileSync(totpFtlFilePath)
|
||||
.toString("utf8")
|
||||
.replace(
|
||||
[
|
||||
" <#list totp.policy.supportedApplications as app>",
|
||||
" <li>${app}</li>",
|
||||
" </#list>"
|
||||
].join("\n"),
|
||||
[
|
||||
" <#if totp.policy.supportedApplications?has_content>",
|
||||
" <#list totp.policy.supportedApplications as app>",
|
||||
" <li>${app}</li>",
|
||||
" </#list>",
|
||||
" </#if>"
|
||||
].join("\n")
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Note, this is an optimization for reducing the size of the jar,
|
||||
// For this version we know exactly which resources are used.
|
||||
{
|
||||
|
203
src/bin/downloadAndUnzip.ts
Normal file
203
src/bin/downloadAndUnzip.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { createHash } from "crypto";
|
||||
import { mkdir, writeFile, unlink } from "fs/promises";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { unzip, zip } from "./tools/unzip";
|
||||
import { rm } from "./tools/fs.rm";
|
||||
import * as child_process from "child_process";
|
||||
import { existsAsync } from "./tools/fs.existsAsync";
|
||||
import type { BuildOptions } from "./keycloakify/buildOptions";
|
||||
import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function downloadAndUnzip(params: {
|
||||
url: string;
|
||||
destDirPath: string;
|
||||
specificDirsToExtract?: string[];
|
||||
preCacheTransform?: {
|
||||
actionCacheId: string;
|
||||
action: (params: { destDirPath: string }) => Promise<void>;
|
||||
};
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
|
||||
|
||||
const { extractDirPath, zipFilePath } = (() => {
|
||||
const zipFileBasenameWithoutExt = generateFileNameFromURL({
|
||||
url,
|
||||
"preCacheTransform":
|
||||
preCacheTransform === undefined
|
||||
? undefined
|
||||
: {
|
||||
"actionCacheId": preCacheTransform.actionCacheId,
|
||||
"actionFootprint": preCacheTransform.action.toString()
|
||||
}
|
||||
});
|
||||
|
||||
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
|
||||
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
|
||||
|
||||
return { zipFilePath, extractDirPath };
|
||||
})();
|
||||
|
||||
download_zip_and_transform: {
|
||||
if (await existsAsync(zipFilePath)) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
const { response, isFromRemoteCache } = await (async () => {
|
||||
const proxyFetchOptions = await getProxyFetchOptions({
|
||||
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
|
||||
proxyFetchOptions
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
response,
|
||||
"isFromRemoteCache": true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"response": await fetch(url, proxyFetchOptions),
|
||||
"isFromRemoteCache": false
|
||||
};
|
||||
})();
|
||||
|
||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||
|
||||
/**
|
||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||
* octokit upgrades.
|
||||
*/
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
|
||||
await writeFile(zipFilePath, response.body);
|
||||
|
||||
if (isFromRemoteCache) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
||||
|
||||
try {
|
||||
await preCacheTransform?.action({
|
||||
"destDirPath": extractDirPath
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await unlink(zipFilePath);
|
||||
|
||||
await zip(extractDirPath, zipFilePath);
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
|
||||
upload_to_remot_cache_if_admin: {
|
||||
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
||||
|
||||
if (githubToken === undefined) {
|
||||
break upload_to_remot_cache_if_admin;
|
||||
}
|
||||
|
||||
console.log("uploading to remote cache");
|
||||
|
||||
try {
|
||||
child_process.execSync(`which putasset`);
|
||||
} catch {
|
||||
child_process.execSync(`npm install -g putasset`);
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execFileSync("putasset", [
|
||||
"--owner",
|
||||
"keycloakify",
|
||||
"--repo",
|
||||
"keycloakify",
|
||||
"--tag",
|
||||
"v0.0.1",
|
||||
"--filename",
|
||||
zipFilePath,
|
||||
"--token",
|
||||
githubToken
|
||||
]);
|
||||
} catch {
|
||||
console.log("upload failed, asset probably already exists in remote cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
function generateFileNameFromURL(params: {
|
||||
url: string;
|
||||
preCacheTransform:
|
||||
| {
|
||||
actionCacheId: string;
|
||||
actionFootprint: string;
|
||||
}
|
||||
| undefined;
|
||||
}): string {
|
||||
const { preCacheTransform } = params;
|
||||
|
||||
// Parse the URL
|
||||
const url = new URL(params.url);
|
||||
|
||||
// Extract pathname and remove leading slashes
|
||||
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
||||
|
||||
// Optionally, add query parameters replacing special characters
|
||||
if (url.search) {
|
||||
fileName += url.search.replace(/[&=?]/g, "-");
|
||||
}
|
||||
|
||||
// Replace any characters that are not valid in filenames
|
||||
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
|
||||
// Trim or pad the fileName to a specific length
|
||||
fileName = fileName.substring(0, 50);
|
||||
|
||||
add_pre_cache_transform: {
|
||||
if (preCacheTransform === undefined) {
|
||||
break add_pre_cache_transform;
|
||||
}
|
||||
|
||||
// Sanitize actionCacheId the same way as other components
|
||||
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
||||
|
||||
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
@ -11,11 +11,14 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
|
||||
import { themeTypes, type ThemeType } from "./constants";
|
||||
import { getReactAppRootDirPath } from "./keycloakify/buildOptions/getReactAppRootDirPath";
|
||||
|
||||
(async () => {
|
||||
console.log("Select a theme type");
|
||||
|
||||
const reactAppRootDirPath = process.cwd();
|
||||
const { reactAppRootDirPath } = getReactAppRootDirPath({
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
"values": [...themeTypes]
|
||||
@ -55,7 +58,7 @@ import { themeTypes, type ThemeType } from "./constants";
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
|
||||
await writeFile(targetFilePath, await readFile(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", pageBasename)));
|
||||
|
||||
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
|
||||
})();
|
||||
|
25
src/bin/keycloakify/buildOptions/UserProvidedBuildOptions.ts
Normal file
25
src/bin/keycloakify/buildOptions/UserProvidedBuildOptions.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type UserProvidedBuildOptions = {
|
||||
extraThemeProperties?: string[];
|
||||
artifactId?: string;
|
||||
groupId?: string;
|
||||
doCreateJar?: boolean;
|
||||
loginThemeResourcesFromKeycloakVersion?: string;
|
||||
reactAppBuildDirPath?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
themeName?: string | string[];
|
||||
doBuildRetrocompatAccountTheme?: boolean;
|
||||
};
|
||||
|
||||
export const zUserProvidedBuildOptions = z.object({
|
||||
"extraThemeProperties": z.array(z.string()).optional(),
|
||||
"artifactId": z.string().optional(),
|
||||
"groupId": z.string().optional(),
|
||||
"doCreateJar": z.boolean().optional(),
|
||||
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
|
||||
"reactAppBuildDirPath": z.string().optional(),
|
||||
"keycloakifyBuildDirPath": z.string().optional(),
|
||||
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
|
||||
"doBuildRetrocompatAccountTheme": z.boolean().optional()
|
||||
});
|
@ -4,7 +4,10 @@ import { join as pathJoin } from "path";
|
||||
import parseArgv from "minimist";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||
import { readResolvedViteConfig } from "./resolvedViteConfig";
|
||||
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
|
||||
import * as fs from "fs";
|
||||
import { getCacheDirPath } from "./getCacheDirPath";
|
||||
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
|
||||
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
|
||||
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = {
|
||||
@ -28,36 +31,31 @@ export type BuildOptions = {
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
doBuildRetrocompatAccountTheme: boolean;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
|
||||
const { processArgv } = params;
|
||||
|
||||
const argv = parseArgv(processArgv);
|
||||
const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
|
||||
|
||||
const reactAppRootDirPath = (() => {
|
||||
const arg = argv["project"] ?? argv["p"];
|
||||
const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
|
||||
|
||||
if (typeof arg !== "string") {
|
||||
return process.cwd();
|
||||
}
|
||||
const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": arg,
|
||||
"cwd": process.cwd()
|
||||
});
|
||||
})();
|
||||
if (resolvedViteConfig === undefined && fs.existsSync(pathJoin(reactAppRootDirPath, "vite.config.ts"))) {
|
||||
throw new Error("Keycloakify's Vite plugin output not found");
|
||||
}
|
||||
|
||||
const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
|
||||
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
|
||||
|
||||
const { resolvedViteConfig } =
|
||||
readResolvedViteConfig({
|
||||
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
|
||||
reactAppRootDirPath
|
||||
}) ?? {};
|
||||
const userProvidedBuildOptions = {
|
||||
...userProvidedBuildOptionsFromPackageJson,
|
||||
...resolvedViteConfig?.userProvidedBuildOptions
|
||||
};
|
||||
|
||||
const themeNames = (() => {
|
||||
if (parsedPackageJson.keycloakify?.themeName === undefined) {
|
||||
if (userProvidedBuildOptions.themeName === undefined) {
|
||||
return [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
@ -66,28 +64,22 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof parsedPackageJson.keycloakify.themeName === "string") {
|
||||
return [parsedPackageJson.keycloakify.themeName];
|
||||
if (typeof userProvidedBuildOptions.themeName === "string") {
|
||||
return [userProvidedBuildOptions.themeName];
|
||||
}
|
||||
|
||||
return parsedPackageJson.keycloakify.themeName;
|
||||
return userProvidedBuildOptions.themeName;
|
||||
})();
|
||||
|
||||
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
|
||||
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
|
||||
reactAppRootDirPath,
|
||||
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack"
|
||||
});
|
||||
|
||||
const reactAppBuildDirPath = (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
|
||||
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
|
||||
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
@ -98,18 +90,22 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
|
||||
})();
|
||||
|
||||
const argv = parseArgv(processArgv);
|
||||
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||
|
||||
return {
|
||||
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
||||
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
||||
themeNames,
|
||||
"extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties,
|
||||
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
|
||||
"groupId": (() => {
|
||||
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
||||
|
||||
return (
|
||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||
parsedPackageJson.keycloakify?.groupId ??
|
||||
userProvidedBuildOptions.groupId ??
|
||||
(parsedPackageJson.homepage === undefined
|
||||
? fallbackGroupId
|
||||
: urlParse(parsedPackageJson.homepage)
|
||||
@ -119,12 +115,24 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`,
|
||||
"doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true,
|
||||
"loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
|
||||
"doCreateJar": userProvidedBuildOptions.doCreateJar ?? true,
|
||||
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
|
||||
reactAppRootDirPath,
|
||||
reactAppBuildDirPath,
|
||||
keycloakifyBuildDirPath,
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
reactAppRootDirPath,
|
||||
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
|
||||
);
|
||||
})(),
|
||||
"publicDirPath": (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
@ -143,19 +151,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
||||
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
|
||||
})(),
|
||||
"cacheDirPath": pathJoin(
|
||||
(() => {
|
||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.XDG_CACHE_HOME,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
|
||||
})(),
|
||||
"keycloakify"
|
||||
),
|
||||
cacheDirPath,
|
||||
"urlPathname": (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
@ -191,6 +187,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
||||
|
||||
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
||||
})(),
|
||||
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
|
||||
"doBuildRetrocompatAccountTheme": userProvidedBuildOptions.doBuildRetrocompatAccountTheme ?? true,
|
||||
npmWorkspaceRootDirPath
|
||||
};
|
||||
}
|
||||
|
25
src/bin/keycloakify/buildOptions/getCacheDirPath.ts
Normal file
25
src/bin/keycloakify/buildOptions/getCacheDirPath.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
|
||||
|
||||
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
|
||||
const { reactAppRootDirPath } = params;
|
||||
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||
|
||||
const cacheDirPath = pathJoin(
|
||||
(() => {
|
||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.XDG_CACHE_HOME,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
|
||||
})(),
|
||||
"keycloakify"
|
||||
);
|
||||
|
||||
return { cacheDirPath };
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||
import { join as pathJoin } from "path";
|
||||
|
||||
export function getKeycloakifyBuildDirPath(params: {
|
||||
reactAppRootDirPath: string;
|
||||
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
|
||||
bundler: "vite" | "webpack";
|
||||
}) {
|
||||
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath, bundler } = params;
|
||||
|
||||
const keycloakifyBuildDirPath = (() => {
|
||||
if (parsedPackageJson_keycloakify_keycloakifyBuildDirPath !== undefined) {
|
||||
getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
reactAppRootDirPath,
|
||||
`${(() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return "dist";
|
||||
case "webpack":
|
||||
return "build";
|
||||
}
|
||||
})()}_keycloak`
|
||||
);
|
||||
})();
|
||||
|
||||
return { keycloakifyBuildDirPath };
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import * as child_process from "child_process";
|
||||
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
let cache:
|
||||
| {
|
||||
reactAppRootDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
|
||||
const { reactAppRootDirPath } = params;
|
||||
|
||||
use_cache: {
|
||||
if (cache === undefined || cache.reactAppRootDirPath !== reactAppRootDirPath) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
const { npmWorkspaceRootDirPath } = cache;
|
||||
|
||||
return { npmWorkspaceRootDirPath };
|
||||
}
|
||||
|
||||
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
||||
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
|
||||
|
||||
try {
|
||||
child_process.execSync("npm config get", { cwd, "stdio": ["pipe", "pipe", "pipe"] });
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
assert(cwd !== pathSep, "NPM workspace not found");
|
||||
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return cwd;
|
||||
})(0);
|
||||
|
||||
cache = {
|
||||
reactAppRootDirPath,
|
||||
npmWorkspaceRootDirPath
|
||||
};
|
||||
|
||||
return { npmWorkspaceRootDirPath };
|
||||
}
|
23
src/bin/keycloakify/buildOptions/getReactAppRootDirPath.ts
Normal file
23
src/bin/keycloakify/buildOptions/getReactAppRootDirPath.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import parseArgv from "minimist";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||
|
||||
export function getReactAppRootDirPath(params: { processArgv: string[] }) {
|
||||
const { processArgv } = params;
|
||||
|
||||
const argv = parseArgv(processArgv);
|
||||
|
||||
const reactAppRootDirPath = (() => {
|
||||
const arg = argv["project"] ?? argv["p"];
|
||||
|
||||
if (typeof arg !== "string") {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": arg,
|
||||
"cwd": process.cwd()
|
||||
});
|
||||
})();
|
||||
|
||||
return { reactAppRootDirPath };
|
||||
}
|
@ -3,41 +3,20 @@ import { assert } from "tsafe";
|
||||
import type { Equals } from "tsafe";
|
||||
import { z } from "zod";
|
||||
import { join as pathJoin } from "path";
|
||||
import { type UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
|
||||
|
||||
export type ParsedPackageJson = {
|
||||
name: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
keycloakify?: {
|
||||
extraThemeProperties?: string[];
|
||||
artifactId?: string;
|
||||
groupId?: string;
|
||||
doCreateJar?: boolean;
|
||||
loginThemeResourcesFromKeycloakVersion?: string;
|
||||
reactAppBuildDirPath?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
themeName?: string | string[];
|
||||
doBuildRetrocompatAccountTheme?: boolean;
|
||||
};
|
||||
keycloakify?: UserProvidedBuildOptions;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
"name": z.string(),
|
||||
"version": z.string().optional(),
|
||||
"homepage": z.string().optional(),
|
||||
"keycloakify": z
|
||||
.object({
|
||||
"extraThemeProperties": z.array(z.string()).optional(),
|
||||
"artifactId": z.string().optional(),
|
||||
"groupId": z.string().optional(),
|
||||
"doCreateJar": z.boolean().optional(),
|
||||
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
|
||||
"reactAppBuildDirPath": z.string().optional(),
|
||||
"keycloakifyBuildDirPath": z.string().optional(),
|
||||
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
|
||||
"doBuildRetrocompatAccountTheme": z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
"keycloakify": zUserProvidedBuildOptions.optional()
|
||||
});
|
||||
|
||||
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
||||
|
@ -5,20 +5,22 @@ import { z } from "zod";
|
||||
import { join as pathJoin } from "path";
|
||||
import { resolvedViteConfigJsonBasename } from "../../constants";
|
||||
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
|
||||
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
|
||||
import { UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
|
||||
|
||||
export type ResolvedViteConfig = {
|
||||
buildDir: string;
|
||||
publicDir: string;
|
||||
assetsDir: string;
|
||||
urlPathname: string | undefined;
|
||||
userProvidedBuildOptions: UserProvidedBuildOptions;
|
||||
};
|
||||
|
||||
const zResolvedViteConfig = z.object({
|
||||
"buildDir": z.string(),
|
||||
"publicDir": z.string(),
|
||||
"assetsDir": z.string(),
|
||||
"urlPathname": z.string().optional()
|
||||
"urlPathname": z.string().optional(),
|
||||
"userProvidedBuildOptions": zUserProvidedBuildOptions
|
||||
});
|
||||
|
||||
{
|
||||
@ -28,31 +30,18 @@ const zResolvedViteConfig = z.object({
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
||||
export function readResolvedViteConfig(params: {
|
||||
reactAppRootDirPath: string;
|
||||
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
|
||||
}):
|
||||
| {
|
||||
resolvedViteConfig: ResolvedViteConfig;
|
||||
}
|
||||
| undefined {
|
||||
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath } = params;
|
||||
export function readResolvedViteConfig(params: { cacheDirPath: string }): {
|
||||
resolvedViteConfig: ResolvedViteConfig | undefined;
|
||||
} {
|
||||
const { cacheDirPath } = params;
|
||||
|
||||
const viteConfigTsFilePath = pathJoin(reactAppRootDirPath, "vite.config.ts");
|
||||
const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
|
||||
|
||||
if (!fs.existsSync(viteConfigTsFilePath)) {
|
||||
return undefined;
|
||||
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
|
||||
return { "resolvedViteConfig": undefined };
|
||||
}
|
||||
|
||||
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
|
||||
reactAppRootDirPath,
|
||||
parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
|
||||
"bundler": "vite"
|
||||
});
|
||||
|
||||
const resolvedViteConfig = (() => {
|
||||
const resolvedViteConfigJsonFilePath = pathJoin(keycloakifyBuildDirPath, resolvedViteConfigJsonBasename);
|
||||
|
||||
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
|
||||
throw new Error("Missing Keycloakify Vite plugin output.");
|
||||
}
|
||||
|
@ -408,6 +408,14 @@
|
||||
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
|
||||
out["pageId"] = "${pageId}";
|
||||
|
||||
try {
|
||||
|
||||
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
|
||||
|
||||
} catch(error) {
|
||||
|
||||
}
|
||||
|
||||
return out;
|
||||
|
||||
})()
|
||||
@ -423,7 +431,7 @@
|
||||
<#if isHash>
|
||||
|
||||
<#if path?size gt 10>
|
||||
<#return "ABORT: Too many recursive calls">
|
||||
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
|
||||
</#if>
|
||||
|
||||
<#local keys = "">
|
||||
@ -455,9 +463,10 @@
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
are_same_path(path, ["url"]) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
@ -480,24 +489,41 @@
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
|
||||
is_subpath(path, ["applications", "applications"]) &&
|
||||
(
|
||||
key == "realm" ||
|
||||
key == "container"
|
||||
)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
"masterAdminClient" == key
|
||||
are_same_path(path, ["user"]) &&
|
||||
key == "delegateForUpdate"
|
||||
) || (
|
||||
<#-- Security audit forwarded by Garth (Gmail) -->
|
||||
are_same_path(path, ["client", "attributes"]) &&
|
||||
key == "saml.signing.private.key"
|
||||
) || (
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
|
||||
are_same_path(path, ["login"]) &&
|
||||
key == "password"
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
["register.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
key == "attemptedUsername" && are_same_path(path, ["auth"])
|
||||
)>
|
||||
<#attempt>
|
||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]>
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
@ -650,9 +676,9 @@
|
||||
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
|
||||
|
||||
</#function>
|
||||
<#function are_same_path path searchedPath>
|
||||
<#function is_subpath path searchedPath>
|
||||
|
||||
<#if path?size != searchedPath?size>
|
||||
<#if path?size < searchedPath?size>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
@ -660,8 +686,14 @@
|
||||
|
||||
<#list path as property>
|
||||
|
||||
<#if i == searchedPath?size >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local searchedProperty=searchedPath[i]>
|
||||
|
||||
<#local i+= 1>
|
||||
|
||||
<#if searchedProperty?is_string && searchedProperty == "*">
|
||||
<#continue>
|
||||
</#if>
|
||||
@ -678,11 +710,13 @@
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#local i+= 1>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return true>
|
||||
|
||||
</#function>
|
||||
|
||||
<#function are_same_path path searchedPath>
|
||||
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
|
||||
</#function>
|
||||
</script>
|
@ -7,7 +7,7 @@ import { join as pathJoin } from "path";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import type { BuildOptions } from "../buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../constants";
|
||||
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
@ -105,7 +105,8 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName),
|
||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common),
|
||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||
"<#if scripts??>",
|
||||
" <#list scripts as script>",
|
||||
|
@ -27,7 +27,15 @@ export const loginThemePageIds = [
|
||||
"saml-post-form.ftl"
|
||||
] as const;
|
||||
|
||||
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
||||
export const accountThemePageIds = [
|
||||
"password.ftl",
|
||||
"account.ftl",
|
||||
"sessions.ftl",
|
||||
"totp.ftl",
|
||||
"applications.ftl",
|
||||
"log.ftl",
|
||||
"federatedIdentity.ftl"
|
||||
] as const;
|
||||
|
||||
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||
|
@ -11,6 +11,7 @@ import { rmSync } from "../../tools/fs.rmSync";
|
||||
type BuildOptionsLike = {
|
||||
keycloakifyBuildDirPath: string;
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ import { rmSync } from "../../tools/fs.rmSync";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
@ -20,6 +20,7 @@ import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import { bringInAccountV1 } from "./bringInAccountV1";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
@ -33,6 +34,7 @@ export type BuildOptionsLike = {
|
||||
urlPathname: string | undefined;
|
||||
doBuildRetrocompatAccountTheme: boolean;
|
||||
themeNames: string[];
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
@ -77,6 +79,11 @@ export async function generateTheme(params: {
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
|
||||
|
||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||
rmSync(destDirPath, { "recursive": true, "force": true });
|
||||
|
||||
if (themeType === "account" && implementedThemeTypes.login) {
|
||||
// NOTE: We prevend doing it twice, it has been done for the login theme.
|
||||
|
||||
@ -88,7 +95,7 @@ export async function generateTheme(params: {
|
||||
"resources",
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
),
|
||||
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir)
|
||||
destDirPath
|
||||
});
|
||||
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
@ -96,7 +103,7 @@ export async function generateTheme(params: {
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": buildOptions.reactAppBuildDirPath,
|
||||
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
|
||||
destDirPath,
|
||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
|
||||
|
@ -6,11 +6,15 @@ import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTe
|
||||
import * as fs from "fs";
|
||||
import { readBuildOptions } from "./buildOptions";
|
||||
import { getLogger } from "../tools/logger";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
|
||||
import { getProjectRoot } from "../tools/getProjectRoot";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
|
||||
import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants";
|
||||
import { assertNoPnpmDlx } from "../tools/assertNoPnpmDlx";
|
||||
|
||||
export async function main() {
|
||||
assertNoPnpmDlx();
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
@ -18,23 +22,15 @@ export async function main() {
|
||||
const logger = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
logger.log("🔏 Building the keycloak theme...⌚");
|
||||
|
||||
const keycloakifyDirPath = getProjectRoot();
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
||||
|
||||
for (const themeName of buildOptions.themeNames) {
|
||||
await generateTheme({
|
||||
themeName,
|
||||
themeSrcDirPath,
|
||||
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
|
||||
buildOptions,
|
||||
"keycloakifyVersion": (() => {
|
||||
const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
|
||||
|
||||
assert(typeof version === "string");
|
||||
|
||||
return version;
|
||||
})()
|
||||
"keycloakifySrcDirPath": pathJoin(getThisCodebaseRootDirPath(), "src"),
|
||||
"keycloakifyVersion": readThisNpmProjectVersion(),
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +40,37 @@ export async function main() {
|
||||
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
|
||||
}
|
||||
|
||||
const containerKeycloakVersion = "23.0.6";
|
||||
|
||||
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
|
||||
|
||||
if (buildOptions.doCreateJar) {
|
||||
generateStartKeycloakTestingContainer({
|
||||
"keycloakVersion": containerKeycloakVersion,
|
||||
jarFilePath,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
run_post_build_script: {
|
||||
if (buildOptions.bundler !== "vite") {
|
||||
break run_post_build_script;
|
||||
}
|
||||
|
||||
child_process.execSync("npx vite", {
|
||||
"cwd": buildOptions.reactAppRootDirPath,
|
||||
"env": {
|
||||
...process.env,
|
||||
[keycloakifyBuildOptionsForPostPostBuildScriptEnvName]: JSON.stringify(buildOptions)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create_jar: {
|
||||
if (!buildOptions.doCreateJar) {
|
||||
break create_jar;
|
||||
}
|
||||
|
||||
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
|
||||
|
||||
const jarDirPath = pathDirname(jarFilePath);
|
||||
@ -67,14 +91,6 @@ export async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
const containerKeycloakVersion = "23.0.0";
|
||||
|
||||
generateStartKeycloakTestingContainer({
|
||||
"keycloakVersion": containerKeycloakVersion,
|
||||
jarFilePath,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
logger.log(
|
||||
[
|
||||
"",
|
||||
|
@ -29,20 +29,6 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
|
||||
);
|
||||
}
|
||||
|
||||
// d={NODE_ENV:"production",PUBLIC_URL:"/foo-bar",WDS_SOCKET_HOST
|
||||
// d={NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST
|
||||
// ->
|
||||
// ... PUBLIC_URL:window.kcContext.url.resourcesPath+"/build" ...
|
||||
fixedJsCode = fixedJsCode.replace(
|
||||
new RegExp(
|
||||
`NODE_ENV:"production",PUBLIC_URL:"${
|
||||
buildOptions.urlPathname !== undefined ? replaceAll(buildOptions.urlPathname.slice(0, -1), "/", "\\/") : ""
|
||||
}"`,
|
||||
"g"
|
||||
),
|
||||
`NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}"`
|
||||
);
|
||||
|
||||
// Example: "static/ or "foo/bar/"
|
||||
const staticDir = (() => {
|
||||
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
|
||||
|
@ -22,11 +22,12 @@ export async function promptKeycloakVersion() {
|
||||
|
||||
const tags = [
|
||||
...(await getLatestsSemVersionedTag({
|
||||
"count": 10,
|
||||
"count": 15,
|
||||
"owner": "keycloak",
|
||||
"repo": "keycloak"
|
||||
}).then(arr => arr.map(({ tag }) => tag))),
|
||||
lastKeycloakVersionWithAccountV1,
|
||||
"19.0.1",
|
||||
"11.0.3"
|
||||
];
|
||||
|
||||
|
14
src/bin/tools/assertNoPnpmDlx.ts
Normal file
14
src/bin/tools/assertNoPnpmDlx.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { sep as pathSep } from "path";
|
||||
|
||||
export function assertNoPnpmDlx() {
|
||||
if (__dirname.includes(`${pathSep}pnpm${pathSep}dlx${pathSep}`)) {
|
||||
console.log(
|
||||
[
|
||||
"Please don't use `pnpm dlx keycloakify`.",
|
||||
"\nUse `npx keycloakify` or `pnpm exec keycloakify` instead since you want to use the",
|
||||
"keycloakify version that is installed in your project and not the latest version on NPM."
|
||||
].join(" ")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
@ -1,301 +0,0 @@
|
||||
import { exec as execCallback } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
import { mkdir, readFile, stat, writeFile, unlink } from "fs/promises";
|
||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep, basename as pathBasename } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { promisify } from "util";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
import { unzip, zip } from "./unzip";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import * as child_process from "child_process";
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
function generateFileNameFromURL(params: {
|
||||
url: string;
|
||||
preCacheTransform:
|
||||
| {
|
||||
actionCacheId: string;
|
||||
actionFootprint: string;
|
||||
}
|
||||
| undefined;
|
||||
}): string {
|
||||
const { preCacheTransform } = params;
|
||||
|
||||
// Parse the URL
|
||||
const url = new URL(params.url);
|
||||
|
||||
// Extract pathname and remove leading slashes
|
||||
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
||||
|
||||
// Optionally, add query parameters replacing special characters
|
||||
if (url.search) {
|
||||
fileName += url.search.replace(/[&=?]/g, "-");
|
||||
}
|
||||
|
||||
// Replace any characters that are not valid in filenames
|
||||
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
|
||||
// Trim or pad the fileName to a specific length
|
||||
fileName = fileName.substring(0, 50);
|
||||
|
||||
add_pre_cache_transform: {
|
||||
if (preCacheTransform === undefined) {
|
||||
break add_pre_cache_transform;
|
||||
}
|
||||
|
||||
// Sanitize actionCacheId the same way as other components
|
||||
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
||||
|
||||
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
async function exists(path: string) {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureArray<T>(arg0: T | T[]) {
|
||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||
}
|
||||
|
||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
|
||||
}
|
||||
|
||||
type NPMConfig = Record<string, string | string[]>;
|
||||
|
||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
|
||||
|
||||
/**
|
||||
* Get npm configuration as map
|
||||
*/
|
||||
async function getNmpConfig() {
|
||||
return readNpmConfig().then(parseNpmConfig);
|
||||
}
|
||||
|
||||
function readNpmConfig(): Promise<string> {
|
||||
return (async function callee(depth: number): Promise<string> {
|
||||
const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
|
||||
|
||||
let stdout: string;
|
||||
|
||||
try {
|
||||
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
assert(cwd !== pathSep);
|
||||
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return stdout;
|
||||
})(0);
|
||||
}
|
||||
|
||||
function parseNpmConfig(stdout: string) {
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2) as [string, string])
|
||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||
}
|
||||
|
||||
function maybeBoolean(arg0: string | undefined) {
|
||||
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
||||
}
|
||||
|
||||
function chunks<T>(arr: T[], size: number = 2) {
|
||||
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
|
||||
}
|
||||
|
||||
async function readCafile(cafile: string) {
|
||||
const cafileContent = await readFile(cafile, "utf-8");
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proxy and ssl configuration from npm config files. Note that we don't care about
|
||||
* proxy config in env vars, because make-fetch-happen will do that for us.
|
||||
*
|
||||
* @returns proxy configuration
|
||||
*/
|
||||
async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "ca" | "cert">> {
|
||||
const cfg = await getNmpConfig();
|
||||
|
||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
||||
const cert = cfg["cert"];
|
||||
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
||||
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
||||
|
||||
if (typeof cafile !== "undefined" && cafile !== "null") ca.push(...(await readCafile(cafile)));
|
||||
|
||||
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
|
||||
}
|
||||
|
||||
export async function downloadAndUnzip(
|
||||
params: {
|
||||
url: string;
|
||||
destDirPath: string;
|
||||
specificDirsToExtract?: string[];
|
||||
preCacheTransform?: {
|
||||
actionCacheId: string;
|
||||
action: (params: { destDirPath: string }) => Promise<void>;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
doUseCache: true;
|
||||
cacheDirPath: string;
|
||||
}
|
||||
| {
|
||||
doUseCache: false;
|
||||
}
|
||||
)
|
||||
) {
|
||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
|
||||
|
||||
const zipFileBasename = generateFileNameFromURL({
|
||||
url,
|
||||
"preCacheTransform":
|
||||
preCacheTransform === undefined
|
||||
? undefined
|
||||
: {
|
||||
"actionCacheId": preCacheTransform.actionCacheId,
|
||||
"actionFootprint": preCacheTransform.action.toString()
|
||||
}
|
||||
});
|
||||
|
||||
const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
|
||||
|
||||
const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
|
||||
const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
|
||||
|
||||
download_zip_and_transform: {
|
||||
if (await exists(zipFilePath)) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
const opts = await getFetchOptions();
|
||||
|
||||
const { response, isFromRemoteCache } = await (async () => {
|
||||
const response = await fetch(`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`, opts);
|
||||
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
response,
|
||||
"isFromRemoteCache": true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"response": await fetch(url, opts),
|
||||
"isFromRemoteCache": false
|
||||
};
|
||||
})();
|
||||
|
||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||
|
||||
/**
|
||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||
* octokit upgrades.
|
||||
*/
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
|
||||
await writeFile(zipFilePath, response.body);
|
||||
|
||||
if (isFromRemoteCache) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
||||
|
||||
try {
|
||||
await preCacheTransform?.action({
|
||||
"destDirPath": extractDirPath
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await unlink(zipFilePath);
|
||||
|
||||
await zip(extractDirPath, zipFilePath);
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
|
||||
upload_to_remot_cache_if_admin: {
|
||||
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
||||
|
||||
if (githubToken === undefined) {
|
||||
break upload_to_remot_cache_if_admin;
|
||||
}
|
||||
|
||||
console.log("uploading to remote cache");
|
||||
|
||||
try {
|
||||
child_process.execSync(`which putasset`);
|
||||
} catch {
|
||||
child_process.execSync(`npm install -g putasset`);
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execFileSync("putasset", [
|
||||
"--owner",
|
||||
"keycloakify",
|
||||
"--repo",
|
||||
"keycloakify",
|
||||
"--tag",
|
||||
"v0.0.1",
|
||||
"--filename",
|
||||
zipFilePath,
|
||||
"--token",
|
||||
githubToken
|
||||
]);
|
||||
} catch {
|
||||
console.log("upload failed, asset probably already exists in remote cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
|
||||
if (!rest.doUseCache) {
|
||||
await rm(cacheDirPath, { "recursive": true });
|
||||
} else {
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
}
|
||||
}
|
73
src/bin/tools/fetchProxyOptions.ts
Normal file
73
src/bin/tools/fetchProxyOptions.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { exec as execCallback } from "child_process";
|
||||
import { readFile } from "fs/promises";
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import { promisify } from "util";
|
||||
|
||||
function ensureArray<T>(arg0: T | T[]) {
|
||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||
}
|
||||
|
||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
|
||||
}
|
||||
|
||||
type NPMConfig = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Get npm configuration as map
|
||||
*/
|
||||
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
|
||||
|
||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2) as [string, string])
|
||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||
}
|
||||
|
||||
export type ProxyFetchOptions = Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca">;
|
||||
|
||||
export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise<ProxyFetchOptions> {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
|
||||
|
||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||
|
||||
function maybeBoolean(arg0: string | undefined) {
|
||||
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
||||
}
|
||||
|
||||
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
||||
const cert = cfg["cert"];
|
||||
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
||||
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
||||
|
||||
if (typeof cafile !== "undefined" && cafile !== "null") {
|
||||
ca.push(
|
||||
...(await (async () => {
|
||||
function chunks<T>(arr: T[], size: number = 2) {
|
||||
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
|
||||
}
|
||||
|
||||
const cafileContent = await readFile(cafile, "utf-8");
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
|
||||
})())
|
||||
);
|
||||
}
|
||||
|
||||
return { proxy, noProxy, strictSSL, cert, "ca": ca.length === 0 ? undefined : ca };
|
||||
}
|
11
src/bin/tools/fs.existsAsync.ts
Normal file
11
src/bin/tools/fs.existsAsync.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as fs from "fs/promises";
|
||||
|
||||
export async function existsAsync(path: string) {
|
||||
try {
|
||||
await fs.stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
function getProjectRootRec(dirPath: string): string {
|
||||
function getThisCodebaseRootDirPath_rec(dirPath: string): string {
|
||||
if (fs.existsSync(path.join(dirPath, "package.json"))) {
|
||||
return dirPath;
|
||||
}
|
||||
return getProjectRootRec(path.join(dirPath, ".."));
|
||||
return getThisCodebaseRootDirPath_rec(path.join(dirPath, ".."));
|
||||
}
|
||||
|
||||
let result: string | undefined = undefined;
|
||||
|
||||
export function getProjectRoot(): string {
|
||||
export function getThisCodebaseRootDirPath(): string {
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return (result = getProjectRootRec(__dirname));
|
||||
return (result = getThisCodebaseRootDirPath_rec(__dirname));
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import { getProjectRoot } from "./getProjectRoot";
|
||||
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||
import { join as pathJoin } from "path";
|
||||
import { constants } from "fs";
|
||||
import { chmod, stat } from "fs/promises";
|
||||
|
||||
(async () => {
|
||||
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json"));
|
||||
|
||||
const promises = Object.values<string>(bin).map(async scriptPath => {
|
||||
const fullPath = pathJoin(getProjectRoot(), scriptPath);
|
||||
const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
|
||||
const oldMode = (await stat(fullPath)).mode;
|
||||
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||
await chmod(fullPath, newMode);
|
||||
|
@ -1,6 +0,0 @@
|
||||
export function pathJoin(...path: string[]): string {
|
||||
return path
|
||||
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
|
||||
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
|
||||
.join(typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/");
|
||||
}
|
12
src/bin/tools/readThisNpmProjectVersion.ts
Normal file
12
src/bin/tools/readThisNpmProjectVersion.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
|
||||
export function readThisNpmProjectVersion(): string {
|
||||
const version = JSON.parse(fs.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")).toString("utf8"))["version"];
|
||||
|
||||
assert(typeof version === "string");
|
||||
|
||||
return version;
|
||||
}
|
44
src/lib/BASE_URL.ts
Normal file
44
src/lib/BASE_URL.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
* WARNING: Internal use only!!
|
||||
* THIS DOES NOT WORK IN KEYCLOAK! It's only for resolving mock assets.
|
||||
* This is just a way to know what's the base url that works
|
||||
* both in webpack and vite.
|
||||
* You can see this as a polyfill that return `import.meta.env.BASE_URL` when in Vite
|
||||
* and when in Webpack returns the base url in the same format as vite does meaning
|
||||
* "/" if hosted at root or "/foo/" when hosted under a subpath (always start and ends with a "/").
|
||||
*/
|
||||
export const BASE_URL = (() => {
|
||||
vite: {
|
||||
let BASE_URL: string;
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
BASE_URL = import.meta.env.BASE_URL;
|
||||
|
||||
assert(typeof BASE_URL === "string");
|
||||
} catch {
|
||||
break vite;
|
||||
}
|
||||
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
webpack: {
|
||||
let BASE_URL: string;
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
BASE_URL = process.env.PUBLIC_URL;
|
||||
|
||||
assert(typeof BASE_URL === "string");
|
||||
} catch {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
return BASE_URL === "" ? "/" : `${BASE_URL}/`;
|
||||
}
|
||||
|
||||
return "/";
|
||||
})();
|
1
src/lib/isStorybook.ts
Normal file
1
src/lib/isStorybook.ts
Normal file
@ -0,0 +1 @@
|
||||
export const isStorybook = typeof window === "object" && Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;
|
@ -36,6 +36,10 @@ export declare namespace keycloak_js {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated: This will be removed in the next major version.
|
||||
* If you use this, please copy paste the code into your project.
|
||||
* Better yet migrate away from keycloak-js and use https://docs.oidc-spa.dev instead.
|
||||
*
|
||||
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
|
||||
* The goal here is just to be able to inject search param in url before keycloak redirect.
|
||||
* Our use case for it is to pass over the login screen the states of useGlobalState
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { headInsert } from "keycloakify/tools/headInsert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export function usePrepareTemplate(params: {
|
||||
doFetchDefaultThemeResources: boolean;
|
||||
@ -8,11 +9,31 @@ export function usePrepareTemplate(params: {
|
||||
scripts?: string[];
|
||||
htmlClassName: string | undefined;
|
||||
bodyClassName: string | undefined;
|
||||
htmlLangProperty?: string | undefined;
|
||||
documentTitle?: string;
|
||||
}) {
|
||||
const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName } = params;
|
||||
const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName, htmlLangProperty, documentTitle } = params;
|
||||
|
||||
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
||||
|
||||
useEffect(() => {
|
||||
if (htmlLangProperty === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = htmlLangProperty;
|
||||
}, [htmlLangProperty]);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentTitle === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.title = documentTitle;
|
||||
}, [documentTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doFetchDefaultThemeResources) {
|
||||
return;
|
||||
|
@ -38,15 +38,15 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": getClassName("kcBodyClass")
|
||||
"bodyClassName": getClassName("kcBodyClass"),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("loginTitle", kcContext.realm.displayName)
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
document.title = i18n.msgStr("loginTitle", kcContext.realm.displayName);
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLoginClass")}>
|
||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
||||
|
@ -84,7 +84,7 @@ export declare namespace KcContext {
|
||||
description?: string;
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
isAppInitiatedAction: boolean;
|
||||
isAppInitiatedAction?: boolean;
|
||||
messagesPerField: {
|
||||
/**
|
||||
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
|
||||
@ -116,6 +116,7 @@ export declare namespace KcContext {
|
||||
*/
|
||||
exists: (fieldName: string) => boolean;
|
||||
};
|
||||
properties: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
export type SamlPostForm = Common & {
|
||||
@ -244,6 +245,17 @@ export declare namespace KcContext {
|
||||
|
||||
export type Terms = Common & {
|
||||
pageId: "terms.ftl";
|
||||
//NOTE: Optional because maybe it wasn't defined in older keycloak versions.
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
attributes: Record<string, string[]>;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
markedForEviction?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginDeviceVerifyUserCode = Common & {
|
||||
|
@ -2,19 +2,19 @@ import type { KcContext, Attribute } from "./KcContext";
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { isStorybook } from "keycloakify/lib/isStorybook";
|
||||
import { id } from "tsafe/id";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { resources_common } from "keycloakify/bin/constants";
|
||||
|
||||
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
mockProperties?: Record<string, string>;
|
||||
}) {
|
||||
const { mockData } = params ?? {};
|
||||
const { mockData, mockProperties } = params ?? {};
|
||||
|
||||
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||
mockPageId?: PageId;
|
||||
@ -31,7 +31,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
warn_that_mock_is_enbaled: {
|
||||
if (isStorybook) {
|
||||
break warn_that_mock_is_enbaled;
|
||||
}
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
}
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
@ -136,6 +142,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
}
|
||||
}
|
||||
|
||||
if (mockProperties !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext.properties,
|
||||
"source": mockProperties
|
||||
});
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
@ -147,8 +160,6 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
|
||||
|
||||
return { "kcContext": realKcContext as any };
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import type { KcContext, Attribute } from "./KcContext";
|
||||
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
|
||||
const attributes: Attribute[] = [
|
||||
{
|
||||
@ -100,9 +100,7 @@ const attributes: Attribute[] = [
|
||||
|
||||
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
|
||||
|
||||
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
|
||||
|
||||
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
|
||||
const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
@ -112,7 +110,7 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
"url": {
|
||||
"loginAction": "#",
|
||||
resourcesPath,
|
||||
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
|
||||
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
|
||||
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
|
||||
},
|
||||
@ -238,7 +236,127 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
"attributes": {}
|
||||
},
|
||||
"scripts": [],
|
||||
"isAppInitiatedAction": false
|
||||
"isAppInitiatedAction": false,
|
||||
"properties": {
|
||||
"kcLogoIdP-facebook": "fa fa-facebook",
|
||||
"parent": "keycloak",
|
||||
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",
|
||||
"kcLogoIdP-bitbucket": "fa fa-bitbucket",
|
||||
"kcAuthenticatorWebAuthnClass": "fa fa-key list-view-pf-icon-lg",
|
||||
"kcWebAuthnDefaultIcon": "pficon pficon-key",
|
||||
"kcLogoIdP-stackoverflow": "fa fa-stack-overflow",
|
||||
"kcSelectAuthListItemClass": "pf-l-stack__item select-auth-box-parent pf-l-split",
|
||||
"kcLogoIdP-microsoft": "fa fa-windows",
|
||||
"kcLocaleItemClass": "pf-c-dropdown__menu-item",
|
||||
"kcLoginOTPListItemHeaderClass": "pf-c-tile__header",
|
||||
"kcLoginOTPListItemIconBodyClass": "pf-c-tile__icon",
|
||||
"kcInputHelperTextAfterClass": "pf-c-form__helper-text pf-c-form__helper-text-after",
|
||||
"kcFormClass": "form-horizontal",
|
||||
"kcSelectAuthListClass": "pf-l-stack select-auth-container",
|
||||
"kcInputClassRadioCheckboxLabelDisabled": "pf-m-disabled",
|
||||
"kcSelectAuthListItemIconClass": "pf-l-split__item select-auth-box-icon",
|
||||
"kcRecoveryCodesWarning": "kc-recovery-codes-warning",
|
||||
"kcFormSettingClass": "login-pf-settings",
|
||||
"kcWebAuthnBLE": "fa fa-bluetooth-b",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcSelectAuthListItemArrowIconClass": "fa fa-angle-right fa-lg",
|
||||
"meta": "viewport==width=device-width,initial-scale=1",
|
||||
"styles": "css/login.css css/tile.css",
|
||||
"kcFeedbackAreaClass": "col-md-12",
|
||||
"kcLogoIdP-google": "fa fa-google",
|
||||
"kcCheckLabelClass": "pf-c-check__label",
|
||||
"kcSelectAuthListItemFillClass": "pf-l-split__item pf-m-fill",
|
||||
"kcAuthenticatorDefaultClass": "fa fa-list list-view-pf-icon-lg",
|
||||
"kcLogoIdP-gitlab": "fa fa-gitlab",
|
||||
"kcFormAreaClass": "col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
|
||||
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcInputClassRadioLabel": "pf-c-radio__label",
|
||||
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
|
||||
"kcSelectAuthListItemHeadingClass": "pf-l-stack__item select-auth-box-headline pf-c-title",
|
||||
"kcInfoAreaClass": "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
|
||||
"kcLogoLink": "http://www.keycloak.org",
|
||||
"kcContainerClass": "container-fluid",
|
||||
"kcSelectAuthListItemTitle": "select-auth-box-paragraph",
|
||||
"kcHtmlClass": "login-pf",
|
||||
"kcLoginOTPListItemTitleClass": "pf-c-tile__title",
|
||||
"locales": "ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
||||
"serviceTitle": "CodeGouv",
|
||||
"kcLogoIdP-openshift-v4": "pf-icon pf-icon-openshift",
|
||||
"kcWebAuthnUnknownIcon": "pficon pficon-key unknown-transport-class",
|
||||
"kcFormSocialAccountNameClass": "kc-social-provider-name",
|
||||
"kcLogoIdP-openshift-v3": "pf-icon pf-icon-openshift",
|
||||
"kcLoginOTPListInputClass": "pf-c-tile__input",
|
||||
"kcWebAuthnUSB": "fa fa-usb",
|
||||
"kcInputClassRadio": "pf-c-radio",
|
||||
"kcWebAuthnKeyIcon": "pficon pficon-key",
|
||||
"kcFeedbackInfoIcon": "fa fa-fw fa-info-circle",
|
||||
"kcCommonLogoIdP": "kc-social-provider-logo kc-social-gray",
|
||||
"stylesCommon":
|
||||
"web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css",
|
||||
"kcRecoveryCodesActions": "kc-recovery-codes-actions",
|
||||
"kcFormGroupHeader": "pf-c-form__group",
|
||||
"kcFormSocialAccountSectionClass": "kc-social-section kc-social-gray",
|
||||
"kcLogoIdP-instagram": "fa fa-instagram",
|
||||
"kcAlertClass": "pf-c-alert pf-m-inline",
|
||||
"kcHeaderClass": "login-pf-page-header",
|
||||
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormSocialAccountLinkClass": "pf-c-login__main-footer-links-item-link",
|
||||
"kcLocaleMainClass": "pf-c-dropdown",
|
||||
"kcTextareaClass": "form-control",
|
||||
"kcButtonBlockClass": "pf-m-block",
|
||||
"kcButtonClass": "pf-c-button",
|
||||
"kcWebAuthnNFC": "fa fa-wifi",
|
||||
"kcLocaleClass": "col-xs-12 col-sm-1",
|
||||
"kcInputClassCheckboxInput": "pf-c-check__input",
|
||||
"kcFeedbackErrorIcon": "fa fa-fw fa-exclamation-circle",
|
||||
"kcInputLargeClass": "input-lg",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
|
||||
"kcRecoveryCodesList": "kc-recovery-codes-list",
|
||||
"kcFormSocialAccountListClass": "pf-c-login__main-footer-links kc-social-links",
|
||||
"kcAlertTitleClass": "pf-c-alert__title kc-feedback-text",
|
||||
"kcAuthenticatorPasswordClass": "fa fa-unlock list-view-pf-icon-lg",
|
||||
"kcCheckInputClass": "pf-c-check__input",
|
||||
"kcLogoIdP-linkedin": "fa fa-linkedin",
|
||||
"kcLogoIdP-twitter": "fa fa-twitter",
|
||||
"kcFeedbackWarningIcon": "fa fa-fw fa-exclamation-triangle",
|
||||
"kcResetFlowIcon": "pficon pficon-arrow fa",
|
||||
"kcSelectAuthListItemIconPropertyClass": "fa-2x select-auth-box-icon-properties",
|
||||
"kcFeedbackSuccessIcon": "fa fa-fw fa-check-circle",
|
||||
"kcLoginOTPListClass": "pf-c-tile",
|
||||
"kcSrOnlyClass": "sr-only",
|
||||
"kcFormSocialAccountListGridClass": "pf-l-grid kc-social-grid",
|
||||
"kcButtonDefaultClass": "btn-default",
|
||||
"kcFormGroupErrorClass": "has-error",
|
||||
"kcSelectAuthListItemDescriptionClass": "pf-l-stack__item select-auth-box-desc",
|
||||
"kcSelectAuthListItemBodyClass": "pf-l-split__item pf-l-stack",
|
||||
"import": "common/keycloak",
|
||||
"kcWebAuthnInternal": "pficon pficon-key",
|
||||
"kcSelectAuthListItemArrowClass": "pf-l-split__item select-auth-box-arrow",
|
||||
"kcCheckClass": "pf-c-check",
|
||||
"kcContentClass": "col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
|
||||
"kcLogoClass": "login-pf-brand",
|
||||
"kcLoginOTPListItemIconClass": "fa fa-mobile",
|
||||
"kcLoginClass": "login-pf-page",
|
||||
"kcSignUpClass": "login-pf-signup",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"kcFormCardClass": "card-pf",
|
||||
"kcLocaleListClass": "pf-c-dropdown__menu pf-m-align-right",
|
||||
"kcInputClass": "pf-c-form-control",
|
||||
"kcFormGroupClass": "form-group",
|
||||
"kcLogoIdP-paypal": "fa fa-paypal",
|
||||
"kcInputClassCheckbox": "pf-c-check",
|
||||
"kcRecoveryCodesConfirmation": "kc-recovery-codes-confirmation",
|
||||
"kcInputClassRadioInput": "pf-c-radio__input",
|
||||
"kcFormSocialAccountListButtonClass": "pf-c-button pf-m-control pf-m-block kc-social-item kc-social-gray",
|
||||
"kcInputClassCheckboxLabel": "pf-c-check__label",
|
||||
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormHeaderClass": "login-pf-header",
|
||||
"kcFormSocialAccountGridItem": "pf-l-grid__item",
|
||||
"kcButtonPrimaryClass": "pf-m-primary",
|
||||
"kcInputHelperTextBeforeClass": "pf-c-form__helper-text pf-c-form__helper-text-before",
|
||||
"kcLogoIdP-github": "fa fa-github",
|
||||
"kcLabelClass": "pf-c-form__label pf-c-form__label-text"
|
||||
}
|
||||
};
|
||||
|
||||
const loginUrl = {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import type { I18n } from "keycloakify/login/i18n";
|
||||
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import type { KcContext } from "keycloakify/account/kcContext";
|
||||
|
||||
export type PageProps<KcContext, I18nExtended extends I18n> = {
|
||||
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
|
||||
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||
kcContext: KcContext;
|
||||
kcContext: NarowedKcContext;
|
||||
i18n: I18nExtended;
|
||||
doUseDefaultCss: boolean;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
|
@ -1,232 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"name": "vite:build-metadata"
|
||||
},
|
||||
{
|
||||
"name": "vite:watch-package-data"
|
||||
},
|
||||
{
|
||||
"name": "vite:pre-alias"
|
||||
},
|
||||
{
|
||||
"name": "alias"
|
||||
},
|
||||
{
|
||||
"name": "vite:react-babel",
|
||||
"enforce": "pre"
|
||||
},
|
||||
{
|
||||
"name": "vite:react-refresh",
|
||||
"enforce": "pre"
|
||||
},
|
||||
{
|
||||
"name": "vite:modulepreload-polyfill"
|
||||
},
|
||||
{
|
||||
"name": "vite:resolve"
|
||||
},
|
||||
{
|
||||
"name": "vite:html-inline-proxy"
|
||||
},
|
||||
{
|
||||
"name": "vite:css"
|
||||
},
|
||||
{
|
||||
"name": "vite:esbuild"
|
||||
},
|
||||
{
|
||||
"name": "vite:json"
|
||||
},
|
||||
{
|
||||
"name": "vite:wasm-helper"
|
||||
},
|
||||
{
|
||||
"name": "vite:worker"
|
||||
},
|
||||
{
|
||||
"name": "vite:asset"
|
||||
},
|
||||
{
|
||||
"name": "vite-plugin-commonjs"
|
||||
},
|
||||
{
|
||||
"name": "keycloakify"
|
||||
},
|
||||
{
|
||||
"name": "vite:wasm-fallback"
|
||||
},
|
||||
{
|
||||
"name": "vite:define"
|
||||
},
|
||||
{
|
||||
"name": "vite:css-post"
|
||||
},
|
||||
{
|
||||
"name": "vite:build-html"
|
||||
},
|
||||
{
|
||||
"name": "vite:worker-import-meta-url"
|
||||
},
|
||||
{
|
||||
"name": "vite:asset-import-meta-url"
|
||||
},
|
||||
{
|
||||
"name": "vite:force-systemjs-wrap-complete"
|
||||
},
|
||||
{
|
||||
"name": "commonjs",
|
||||
"version": "25.0.7"
|
||||
},
|
||||
{
|
||||
"name": "vite:data-uri"
|
||||
},
|
||||
{
|
||||
"name": "vite:dynamic-import-vars"
|
||||
},
|
||||
{
|
||||
"name": "vite:import-glob"
|
||||
},
|
||||
{
|
||||
"name": "vite:build-import-analysis"
|
||||
},
|
||||
{
|
||||
"name": "vite:esbuild-transpile"
|
||||
},
|
||||
{
|
||||
"name": "vite:terser"
|
||||
},
|
||||
{
|
||||
"name": "vite:reporter"
|
||||
},
|
||||
{
|
||||
"name": "vite:load-fallback"
|
||||
}
|
||||
],
|
||||
"optimizeDeps": {
|
||||
"disabled": "build",
|
||||
"esbuildOptions": {
|
||||
"preserveSymlinks": false,
|
||||
"jsx": "automatic",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "vite-plugin-commonjs:pre-bundle"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["react", "react/jsx-dev-runtime", "react/jsx-runtime"]
|
||||
},
|
||||
"build": {
|
||||
"target": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
|
||||
"cssTarget": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
|
||||
"outDir": "dist",
|
||||
"assetsDir": "assets",
|
||||
"assetsInlineLimit": 4096,
|
||||
"cssCodeSplit": true,
|
||||
"sourcemap": false,
|
||||
"rollupOptions": {},
|
||||
"minify": "esbuild",
|
||||
"terserOptions": {},
|
||||
"write": true,
|
||||
"emptyOutDir": null,
|
||||
"copyPublicDir": true,
|
||||
"manifest": false,
|
||||
"lib": false,
|
||||
"ssr": false,
|
||||
"ssrManifest": false,
|
||||
"ssrEmitAssets": false,
|
||||
"reportCompressedSize": true,
|
||||
"chunkSizeWarningLimit": 500,
|
||||
"watch": null,
|
||||
"commonjsOptions": {
|
||||
"include": [{}],
|
||||
"extensions": [".js", ".cjs"]
|
||||
},
|
||||
"dynamicImportVarsOptions": {
|
||||
"warnOnError": true,
|
||||
"exclude": [{}]
|
||||
},
|
||||
"modulePreload": {
|
||||
"polyfill": true
|
||||
},
|
||||
"cssMinify": true
|
||||
},
|
||||
"esbuild": {
|
||||
"jsxDev": false,
|
||||
"jsx": "automatic"
|
||||
},
|
||||
"resolve": {
|
||||
"mainFields": ["browser", "module", "jsnext:main", "jsnext"],
|
||||
"conditions": [],
|
||||
"extensions": [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"],
|
||||
"dedupe": ["react", "react-dom"],
|
||||
"preserveSymlinks": false,
|
||||
"alias": [
|
||||
{
|
||||
"find": {},
|
||||
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/env.mjs"
|
||||
},
|
||||
{
|
||||
"find": {},
|
||||
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/client.mjs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configFile": "/Users/joseph/github/keycloakify-starter/vite.config.ts",
|
||||
"configFileDependencies": ["/Users/joseph/github/keycloakify-starter/vite.config.ts"],
|
||||
"inlineConfig": {
|
||||
"optimizeDeps": {},
|
||||
"build": {}
|
||||
},
|
||||
"root": "/Users/joseph/github/keycloakify-starter",
|
||||
"base": "/",
|
||||
"rawBase": "/",
|
||||
"publicDir": "/Users/joseph/github/keycloakify-starter/public",
|
||||
"cacheDir": "/Users/joseph/github/keycloakify-starter/node_modules/.vite",
|
||||
"command": "build",
|
||||
"mode": "production",
|
||||
"ssr": {
|
||||
"target": "node",
|
||||
"optimizeDeps": {
|
||||
"disabled": true,
|
||||
"esbuildOptions": {
|
||||
"preserveSymlinks": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"isWorker": false,
|
||||
"mainConfig": null,
|
||||
"isProduction": true,
|
||||
"css": {},
|
||||
"server": {
|
||||
"preTransformRequests": true,
|
||||
"middlewareMode": false,
|
||||
"fs": {
|
||||
"strict": true,
|
||||
"allow": ["/Users/joseph/github/keycloakify-starter"],
|
||||
"deny": [".env", ".env.*", "*.{crt,pem}"],
|
||||
"cachedChecks": false
|
||||
}
|
||||
},
|
||||
"preview": {},
|
||||
"envDir": "/Users/joseph/github/keycloakify-starter",
|
||||
"env": {
|
||||
"BASE_URL": "/",
|
||||
"MODE": "production",
|
||||
"DEV": false,
|
||||
"PROD": true
|
||||
},
|
||||
"logger": {
|
||||
"hasWarned": false
|
||||
},
|
||||
"packageCache": {},
|
||||
"worker": {
|
||||
"format": "iife",
|
||||
"rollupOptions": {}
|
||||
},
|
||||
"appType": "spa",
|
||||
"experimental": {
|
||||
"importGlobRestoreExtension": false,
|
||||
"hmrPartialAccept": false
|
||||
}
|
||||
}
|
@ -1,28 +1,74 @@
|
||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||
import { readParsedPackageJson } from "../bin/keycloakify/buildOptions/parsedPackageJson";
|
||||
import type { Plugin } from "vite";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { resolvedViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, keycloak_resources } from "../bin/constants";
|
||||
import {
|
||||
resolvedViteConfigJsonBasename,
|
||||
nameOfTheGlobal,
|
||||
basenameOfTheKeycloakifyResourcesDir,
|
||||
keycloak_resources,
|
||||
keycloakifyBuildOptionsForPostPostBuildScriptEnvName
|
||||
} from "../bin/constants";
|
||||
import type { ResolvedViteConfig } from "../bin/keycloakify/buildOptions/resolvedViteConfig";
|
||||
import { getKeycloakifyBuildDirPath } from "../bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath";
|
||||
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
|
||||
import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
|
||||
import { id } from "tsafe/id";
|
||||
import { rm } from "../bin/tools/fs.rm";
|
||||
import { copyKeycloakResourcesToPublic } from "../bin/copy-keycloak-resources-to-public";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../bin/keycloakify/buildOptions";
|
||||
import type { UserProvidedBuildOptions } from "../bin/keycloakify/buildOptions/UserProvidedBuildOptions";
|
||||
import MagicString from "magic-string";
|
||||
|
||||
export type Params = UserProvidedBuildOptions & {
|
||||
postBuild?: (buildOptions: Omit<BuildOptions, "bundler">) => Promise<void>;
|
||||
};
|
||||
|
||||
export function keycloakify(params?: Params) {
|
||||
const { postBuild, ...userProvidedBuildOptions } = params ?? {};
|
||||
|
||||
export function keycloakify(): Plugin {
|
||||
let reactAppRootDirPath: string | undefined = undefined;
|
||||
let urlPathname: string | undefined = undefined;
|
||||
let buildDirPath: string | undefined = undefined;
|
||||
let command: "build" | "serve" | undefined = undefined;
|
||||
let shouldGenerateSourcemap: boolean | undefined = undefined;
|
||||
|
||||
const plugin = {
|
||||
"name": "keycloakify" as const,
|
||||
"configResolved": async resolvedConfig => {
|
||||
shouldGenerateSourcemap = resolvedConfig.build.sourcemap !== false;
|
||||
|
||||
run_post_build_script: {
|
||||
const buildOptionJson = process.env[keycloakifyBuildOptionsForPostPostBuildScriptEnvName];
|
||||
|
||||
if (buildOptionJson === undefined) {
|
||||
break run_post_build_script;
|
||||
}
|
||||
|
||||
if (postBuild === undefined) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const buildOptions: BuildOptions = JSON.parse(buildOptionJson);
|
||||
|
||||
await postBuild(buildOptions);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
command = resolvedConfig.command;
|
||||
|
||||
return {
|
||||
"name": "keycloakify",
|
||||
"apply": "build",
|
||||
"configResolved": resolvedConfig => {
|
||||
reactAppRootDirPath = resolvedConfig.root;
|
||||
urlPathname = (() => {
|
||||
let out = resolvedConfig.env.BASE_URL;
|
||||
|
||||
if (out.startsWith(".") && command === "build" && resolvedConfig.envPrefix?.includes("STORYBOOK_") !== true) {
|
||||
throw new Error(
|
||||
[
|
||||
`BASE_URL=${out} is not supported By Keycloakify. Use an absolute URL instead.`,
|
||||
`If this is a problem, please open an issue at https://github.com/keycloakify/keycloakify/issues/new`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (out === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@ -40,26 +86,24 @@ export function keycloakify(): Plugin {
|
||||
|
||||
buildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
|
||||
|
||||
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
|
||||
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": readParsedPackageJson({ reactAppRootDirPath }).keycloakify
|
||||
?.keycloakifyBuildDirPath,
|
||||
reactAppRootDirPath,
|
||||
"bundler": "vite"
|
||||
const { cacheDirPath } = getCacheDirPath({
|
||||
reactAppRootDirPath
|
||||
});
|
||||
|
||||
if (!fs.existsSync(keycloakifyBuildDirPath)) {
|
||||
fs.mkdirSync(keycloakifyBuildDirPath);
|
||||
if (!fs.existsSync(cacheDirPath)) {
|
||||
fs.mkdirSync(cacheDirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakifyBuildDirPath, resolvedViteConfigJsonBasename),
|
||||
pathJoin(cacheDirPath, resolvedViteConfigJsonBasename),
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
id<ResolvedViteConfig>({
|
||||
"publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
|
||||
"assetsDir": resolvedConfig.build.assetsDir,
|
||||
"buildDir": resolvedConfig.build.outDir,
|
||||
urlPathname
|
||||
urlPathname,
|
||||
userProvidedBuildOptions
|
||||
}),
|
||||
null,
|
||||
2
|
||||
@ -67,62 +111,82 @@ export function keycloakify(): Plugin {
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
await copyKeycloakResourcesToPublic({
|
||||
"processArgv": ["--project", reactAppRootDirPath]
|
||||
});
|
||||
},
|
||||
"transform": (code, id) => {
|
||||
assert(reactAppRootDirPath !== undefined);
|
||||
assert(command !== undefined);
|
||||
assert(shouldGenerateSourcemap !== undefined);
|
||||
|
||||
let transformedCode: string | undefined = undefined;
|
||||
|
||||
replace_import_meta_env_base_url_in_source_code: {
|
||||
{
|
||||
const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
|
||||
|
||||
if (!isWithinSourceDirectory) {
|
||||
break replace_import_meta_env_base_url_in_source_code;
|
||||
}
|
||||
}
|
||||
|
||||
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
|
||||
|
||||
{
|
||||
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
|
||||
|
||||
if (!isTypeScriptFile && !isJavascriptFile) {
|
||||
break replace_import_meta_env_base_url_in_source_code;
|
||||
}
|
||||
}
|
||||
|
||||
const windowToken = isJavascriptFile ? "window" : "(window as any)";
|
||||
|
||||
if (transformedCode === undefined) {
|
||||
transformedCode = code;
|
||||
}
|
||||
|
||||
transformedCode = replaceAll(
|
||||
transformedCode,
|
||||
"import.meta.env.BASE_URL",
|
||||
[
|
||||
`(`,
|
||||
`(${windowToken}.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development") ?`,
|
||||
` "${urlPathname ?? "/"}" :`,
|
||||
` \`\${${windowToken}.${nameOfTheGlobal}.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/\``,
|
||||
`)`
|
||||
].join("")
|
||||
);
|
||||
}
|
||||
|
||||
if (transformedCode === undefined) {
|
||||
if (command !== "build") {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(reactAppRootDirPath !== undefined);
|
||||
|
||||
{
|
||||
const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
|
||||
|
||||
if (!isWithinSourceDirectory) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
|
||||
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
|
||||
|
||||
if (!isTypeScriptFile && !isJavascriptFile) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const transformedCode = new MagicString(code);
|
||||
|
||||
transformedCode.replaceAll(
|
||||
/import\.meta\.env(?:(?:\.BASE_URL)|(?:\["BASE_URL"\]))/g,
|
||||
[
|
||||
`(`,
|
||||
`(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`,
|
||||
`"${urlPathname ?? "/"}":`,
|
||||
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
|
||||
`)`
|
||||
].join("")
|
||||
);
|
||||
|
||||
if (!transformedCode.hasChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldGenerateSourcemap) {
|
||||
return transformedCode.toString();
|
||||
}
|
||||
|
||||
const map = transformedCode.generateMap({
|
||||
"source": id,
|
||||
"includeContent": true,
|
||||
"hires": true
|
||||
});
|
||||
|
||||
return {
|
||||
"code": transformedCode
|
||||
"code": transformedCode.toString(),
|
||||
"map": map.toString()
|
||||
};
|
||||
},
|
||||
"buildEnd": async () => {
|
||||
"closeBundle": async () => {
|
||||
assert(command !== undefined);
|
||||
|
||||
if (command !== "build") {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(buildDirPath !== undefined);
|
||||
|
||||
await rm(pathJoin(buildDirPath, keycloak_resources), { "recursive": true, "force": true });
|
||||
}
|
||||
};
|
||||
} satisfies Plugin;
|
||||
|
||||
return plugin as any;
|
||||
}
|
||||
|
@ -1,24 +1,19 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "account.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `account/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: "account/Account",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
173
stories/account/pages/Authenticator.stories.tsx
Normal file
173
stories/account/pages/Authenticator.stories.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "totp.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "account/Authenticator",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
enabled: false,
|
||||
totpSecretEncoded: "HE4W MSTC OBKU CY2M ONXF OV3Q NYYU I3SH",
|
||||
totpSecret: "99fJbpUAcLsnWWpn1DnG",
|
||||
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACk0lEQVR4Xu2YQY6DMAxFjViw5AjcBC6GBBIXozfJEbpkger53wEKqOpmFvaikQYNeVRyHPvbiejXscp95jp+/D5zHT9+n7kO8qeIFDqKzjJo9dC1wUSPP7yG4IPq41lq9ZK+keLZSwXDGwMhOCZgdX4sBVD1qld+GYg/h6ScreBuIDo5FKfVM7Z8aWs9PB2E2/73DdOlwUrK9Ck+HDnzB7ziR8fjlD/OPI8pVQwCi899TkNw2M+tp9XSLFKPIq2UySIhBB906fCQTicFwiv1EUG6+d+bl4zPIYnUk5oIcS69/evPYStUp6P0dJhD/mhauijcth76mOsfw+GFrbfXKJx7LW2N15kijuWIMCYicLQOCEimDp1c0L8PzCLTs3/d+ZQLyl6VqeSIT9nz25szf2ZybHgC31yrXEQIbqaPjX0k9GqWy0N/nLkagsHWNXR0LZwsR357c0pjC6fm+meu5f6f6oszz/qj7GpYCdHf0LVH/gTgtJ/5bVavPJ9svwnBS9qaqwoHOh3G7Ln++HIIDgpKYpFW00dlkX7ruz836THBWQpzd23/xeDsFVroz15fRjsfMyaC8JX2Y8PZf+VIoKff+uTO6WSIUIfSkrl9/rbfnbPr30R8hnMtXA/98ea5lx4ZlSMgQlMsEnb73XnP+yNl/SuR3/lzTSZHMTirMpMcXjWr0U5Mp/rnzmk/TsXkC2/iKEJ5TRG4DZ5KrP/C0RiVmkp+5I8zN1uh2vv9Vs+bzJ4947Y+bz6wl6ZIcv87ZaU2+6PwnoKdb7VYmrf9Z02MxCmNdmparbVJtrA4nA+e9LgIS6dzfvly7j+4XWIuPJp8iE9PbvkzJHYNabt/o5MP+535t/Hj95nr+PH7zHX8m/8B+RAnloz5pi4AAAAASUVORK5CYII=",
|
||||
policy: {
|
||||
type: "totp"
|
||||
},
|
||||
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
|
||||
otpCredentials: []
|
||||
},
|
||||
messagesPerField: {},
|
||||
stateChecker: "ihTeSAMfNsobnPjYiktV8DY-5T4sVzVdrEZRdwfMm8Y",
|
||||
realm: {
|
||||
userManagedAccessAllowed: true,
|
||||
internationalizationEnabled: false
|
||||
},
|
||||
url: {
|
||||
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
|
||||
},
|
||||
keycloakifyVersion: "9.6.1",
|
||||
themeVersion: "1.0.10",
|
||||
themeType: "account",
|
||||
themeName: "keycloakify",
|
||||
pageId: "totp.ftl"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithTotpEnabled = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "G55E MZKC JFUD MQLT MFIF EVSB JFLG M6SO",
|
||||
totpSecret: "7zFeBIh6AsaPRVAIVfzN",
|
||||
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
|
||||
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACo0lEQVR4Xu2YPY6DMBCFJ6JwyRF8k+RiSCBxsXATjkDpAmX2vTGBwErbbDFTZIps4s8rmfl9RvRPW+W6crYvv66c7cuvK2cjX0Tkho9yW/q5PHSc5QYA62PwXnWqmzrRSUdNL+mygRC8kzQZWhqVO1CRds3YHopnfUkzp2c7ZAY+GIdXywOb0qsdJMXiFn9serYrncxNv/PDkdfUzObk/eNaX368mnl1kML8RH1vFoGzargA1DM/VeWhOpf9+by5iL5Q0NaEUETslHiSIz+dOc4q0tqBrcg7IsnpnZ8BeLmjqjFa4Fps4vlR3484nFHH6OP8o1cTc4I/Q3D4Uqw1TjpkeHqc2R/Rjvb89OUUDAL/CpycOf/o6fUjP505/phrOf8wn+tolsxyD8GZnzyrJSScrNyEcXhHJwrBh2yj2fShPlFB2PQxn935aK1HIB1G1nczm8+P+nbmC7si+zell53a4i97fnhz5Gddxc9iSgLPpPifGn9vDqN0YBL0lpozdx7nd+dDHSiFXkV+NlZO85Efzvzda8yrwkylvlEbhxE4bTJpiCEIkWNHbxD/w/++fJMOVX8p5Q70F0V2EI4LsUWd+ov6Wtgu5aM/OXNIf6jWbKq6zmekA77t88WZr5lXO6vvWaj6kbNo4nv/ceaon0TpYPqrmNJhue/x9+ZKLchbO+cLPrb+aI09BLeob1en2nqkKsUYfOvatSGa/ircmD7i78rNmJoYzXwIKh228z3+ztzef+Cb6S/lSxoWOXM2CO/ZuvlqARtLvX8u1Ie6+d+bd/X9pdS3lrrF/8jPCPytv9AVIbfvddxE4iNFLKL+hH/xCNudKgTvGX/r33ars/y062gQjljfWN8cyKm+f2NPOvqTL//Lvvy6crYvv66c7d/8B/9RFjk6Tp30AAAAAElFTkSuQmCC",
|
||||
policy: {
|
||||
type: "totp",
|
||||
algorithm: "HmacSHA1"
|
||||
},
|
||||
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
|
||||
otpCredentials: [
|
||||
{
|
||||
id: "7afaaf7d-f2d5-44f5-a966-e5297f0b2b7a",
|
||||
userLabel: "mobile"
|
||||
}
|
||||
]
|
||||
},
|
||||
message: {
|
||||
summary: "Mobile authenticator configured.",
|
||||
type: "success"
|
||||
},
|
||||
url: {
|
||||
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
|
||||
},
|
||||
messagesPerField: {},
|
||||
stateChecker: "0UvyCNJHRJXmdahtRmn0tTPCU2nwLtWBUfPaaX1qb4g",
|
||||
realm: {
|
||||
userManagedAccessAllowed: true,
|
||||
internationalizationEnabled: false
|
||||
},
|
||||
keycloakifyVersion: "9.6.1",
|
||||
themeVersion: "1.0.10",
|
||||
themeType: "account",
|
||||
themeName: "keycloakify",
|
||||
pageId: "totp.ftl"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithManualMode = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
mode: "manual",
|
||||
totp: {
|
||||
enabled: false,
|
||||
totpSecretEncoded: "KZ5H CYTW GBVV ASDE JRXG MMCK HAZU E6TX",
|
||||
totpSecret: "Vzqbv0kPHdLnf0J83Bzw",
|
||||
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACpklEQVR4Xu2YQa6rMAxFXTFgyBLYSbsxJJC6sb6dZAkdMkDNv8cBStHTm/yBM2ikIpqTgePY1w6W/xyLnWc+x5efZz7Hl59nPgf8aWaXPFl+2ZhbzfWaGPTT3yr4mPPPs8nty4ZeKxfzRQ6q4IO1P8zq0c/iffvqtIlLTfw5psxsK3f7JirjTHDqWpQ3T9fC/fytn2956u32bNJv8RHIyZ/n0MvJh8cpvwJ5GffkQaBNYPo2auCyv30YVmtitm4yu1qT5mtXCR9svsqXeih1/I1IbZHLKniTskxPOvCGSB3Wud2/0Vz+5YH9uHZAvzORUAlXaXmY9FHxyZuWI0L5sfs3lkt1vDTbtVtM8bmovrCT26o/0bxozVAWIY3IuTLpsvk3mDNeRv9QqrJWEp+25Xc01/uMVudHpySiE3PXklN1cLSm8yCgKmuWICUIxip4vqM6Y+kalNX3hJNtz+9orgOXQ60noZPrd/H5u74E86I/pfXXm/obXPvOr2juVW8o9nsTS77T5Ix18CZ71sh+qQ7n3+LzY32J5WptXt291Bdaf8tcVw76Hcvpqr31R3CUOri7Q79r4ap61+5O12XoT1leOrFK+HZ/asga/sr0tz5F85wozWq4aMKcP1DK3f54Ttfv+a0iqG1wCU2H/iGWl156IionQYWmngTpan84H9aGy+8nl7I8J5ejOnjP0SNCC/0/lVpydKyPwZz7u/Xef80ouaRHHt7PP5j74BJFfBpJ3vLp460/wdxtxX5KM6XPMvktJ6/7i+YjvfRS/Gs3za3218LJH5qwzKKf7fzd3fXwEWmkf5WTKS3JN1YRTxKhiY9IC6mzUKmP/g3knL8cqoeUiKvJL/EZyT1/sJ/vg+X7G07e7Q/mf40vP898ji8/z3yO/+b/ANUwOXCzdQgqAAAAAElFTkSuQmCC",
|
||||
policy: {
|
||||
type: "totp",
|
||||
algorithm: "HmacSHA1"
|
||||
},
|
||||
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
|
||||
otpCredentials: []
|
||||
},
|
||||
messagesPerField: {},
|
||||
stateChecker: "HiBl2ADzLwKwQS813LOEig1Ymm4xpEu_NacYtWJIuHU",
|
||||
realm: {
|
||||
userManagedAccessAllowed: true,
|
||||
internationalizationEnabled: false
|
||||
},
|
||||
url: {
|
||||
totpUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual"
|
||||
},
|
||||
keycloakifyVersion: "9.6.1",
|
||||
themeVersion: "1.0.10",
|
||||
themeType: "account",
|
||||
themeName: "keycloakify",
|
||||
pageId: "totp.ftl"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const MoreThanOneTotpProviders = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "G55E MZKC JFUD MQLT MFIF EVSB JFLG M6SO",
|
||||
totpSecret: "7zFeBIh6AsaPRVAIVfzN",
|
||||
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
|
||||
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACo0lEQVR4Xu2YPY6DMBCFJ6JwyRF8k+RiSCBxsXATjkDpAmX2vTGBwErbbDFTZIps4s8rmfl9RvRPW+W6crYvv66c7cuvK2cjX0Tkho9yW/q5PHSc5QYA62PwXnWqmzrRSUdNL+mygRC8kzQZWhqVO1CRds3YHopnfUkzp2c7ZAY+GIdXywOb0qsdJMXiFn9serYrncxNv/PDkdfUzObk/eNaX368mnl1kML8RH1vFoGzargA1DM/VeWhOpf9+by5iL5Q0NaEUETslHiSIz+dOc4q0tqBrcg7IsnpnZ8BeLmjqjFa4Fps4vlR3484nFHH6OP8o1cTc4I/Q3D4Uqw1TjpkeHqc2R/Rjvb89OUUDAL/CpycOf/o6fUjP505/phrOf8wn+tolsxyD8GZnzyrJSScrNyEcXhHJwrBh2yj2fShPlFB2PQxn935aK1HIB1G1nczm8+P+nbmC7si+zell53a4i97fnhz5Gddxc9iSgLPpPifGn9vDqN0YBL0lpozdx7nd+dDHSiFXkV+NlZO85Efzvzda8yrwkylvlEbhxE4bTJpiCEIkWNHbxD/w/++fJMOVX8p5Q70F0V2EI4LsUWd+ov6Wtgu5aM/OXNIf6jWbKq6zmekA77t88WZr5lXO6vvWaj6kbNo4nv/ceaon0TpYPqrmNJhue/x9+ZKLchbO+cLPrb+aI09BLeob1en2nqkKsUYfOvatSGa/ircmD7i78rNmJoYzXwIKh228z3+ztzef+Cb6S/lSxoWOXM2CO/ZuvlqARtLvX8u1Ie6+d+bd/X9pdS3lrrF/8jPCPytv9AVIbfvddxE4iNFLKL+hH/xCNudKgTvGX/r33ars/y062gQjljfWN8cyKm+f2NPOvqTL//Lvvy6crYvv66c7d/8B/9RFjk6Tp30AAAAAElFTkSuQmCC",
|
||||
policy: {
|
||||
type: "totp",
|
||||
algorithm: "HmacSHA1"
|
||||
},
|
||||
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
|
||||
otpCredentials: [
|
||||
{
|
||||
id: "7afaaf7d-f2d5-44f5-a966-e5297f0b2b7a",
|
||||
userLabel: "Samsung S23"
|
||||
},
|
||||
{
|
||||
id: "fbe22500-d979-45a3-9666-84c99e27958e",
|
||||
userLabel: "Apple Iphone 15"
|
||||
}
|
||||
]
|
||||
},
|
||||
url: {
|
||||
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
|
||||
},
|
||||
messagesPerField: {},
|
||||
stateChecker: "0UvyCNJHRJXmdahtRmn0tTPCU2nwLtWBUfPaaX1qb4g",
|
||||
realm: {
|
||||
userManagedAccessAllowed: true,
|
||||
internationalizationEnabled: false
|
||||
},
|
||||
keycloakifyVersion: "9.6.1",
|
||||
themeVersion: "1.0.10",
|
||||
themeType: "account",
|
||||
themeName: "keycloakify",
|
||||
pageId: "totp.ftl"
|
||||
}}
|
||||
/>
|
||||
);
|
34
stories/account/pages/FederatedIdentity.stories.tsx
Normal file
34
stories/account/pages/FederatedIdentity.stories.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "federatedIdentity.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta = {
|
||||
title: "account/FederatedIdentity",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
||||
|
||||
export const NotConnected = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
pageId: "federatedIdentity.ftl",
|
||||
federatedIdentity: {
|
||||
identities: [
|
||||
{
|
||||
providerId: "google",
|
||||
displayName: "keycloak-oidc",
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
removeLinkPossible: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
354
stories/account/pages/Log.stories.tsx
Normal file
354
stories/account/pages/Log.stories.tsx
Normal file
@ -0,0 +1,354 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "log.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "account/Log",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
log: {
|
||||
events: [
|
||||
{
|
||||
date: "2024-04-26T12:29:08Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T12:10:56Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:57:34Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "update totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:57:21Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "update totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:56:56Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "remove totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:56:55Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "remove totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:56:41Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "update totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:56:36Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [],
|
||||
event: "update totp"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T11:32:54Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:42:54Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:42:52Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:42:40Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:42:09Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "false",
|
||||
key: "remember_me"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:24:17Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [],
|
||||
event: "logout"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:23:54Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:23:50Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:23:47Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:23:15Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [],
|
||||
event: "logout"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:23:06Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:22:53Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [],
|
||||
event: "logout"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:21:29Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "false",
|
||||
key: "remember_me"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-26T09:17:32Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-18T11:19:09Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-18T11:18:50Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "keycloakify-frontend",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
},
|
||||
{
|
||||
date: "2024-04-18T11:18:24Z",
|
||||
ipAddress: "127.0.0.1",
|
||||
client: "account",
|
||||
details: [
|
||||
{
|
||||
value: "openid-connect",
|
||||
key: "auth_method"
|
||||
},
|
||||
{
|
||||
value: "john.doe",
|
||||
key: "username"
|
||||
}
|
||||
],
|
||||
event: "login"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
@ -1,23 +1,15 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "password.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `account/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const meta = {
|
||||
title: "account/Password",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
|
55
stories/account/pages/Sessions.stories.tsx
Normal file
55
stories/account/pages/Sessions.stories.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "sessions.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta = {
|
||||
title: "account/Sessions",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
sessions: {
|
||||
sessions: [
|
||||
{
|
||||
expires: "2024-04-26T18:14:19Z",
|
||||
clients: ["account"],
|
||||
ipAddress: "172.20.0.1",
|
||||
started: "2024-04-26T08:14:19Z",
|
||||
lastAccess: "2024-04-26T08:30:54Z",
|
||||
id: "af835e30-4821-43b1-b4f7-e732d3cc15d2"
|
||||
},
|
||||
{
|
||||
expires: "2024-04-26T18:14:09Z",
|
||||
clients: ["security-admin-console", "account"],
|
||||
ipAddress: "172.20.0.1",
|
||||
started: "2024-04-26T08:14:09Z",
|
||||
lastAccess: "2024-04-26T08:15:14Z",
|
||||
id: "60a9d8b8-617d-441e-8643-08c3fe30e231"
|
||||
}
|
||||
]
|
||||
},
|
||||
stateChecker: "xQ7EOgFrLi4EvnJ8dbXKhwFGWk_bkOp0X89mhilt1os"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithError = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
url: { passwordUrl: "/auth/realms/keycloakify/account/password" },
|
||||
stateChecker: "xQ7EOgFrLi4EvnJ8dbXKhwFGWk_bkOp0X89mhilt1os",
|
||||
message: {
|
||||
summary: "Invalid existing password.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
105
stories/account/pages/Totp.stories.tsx
Normal file
105
stories/account/pages/Totp.stories.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "totp.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta = {
|
||||
title: "account/Authenticator",
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
enabled: false,
|
||||
totpSecretEncoded: "HB2W ESCK KJKF K5DC GJQX S5RQ I5AX CZ2U",
|
||||
totpSecret: "8ubHJRTUtb2ayv0GAqgT",
|
||||
manualUrl: "http://localhost:8080/realms/master/account/totp?mode=manual",
|
||||
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACoUlEQVR4Xu2YPW7DMAxGGWTImCP4JvbFAtiAL+bcxEfI6MEI+z3Kzl+BLh2oIRwEV08FJIr8SMX8T1vtc+bdvvxz5t2+/HPm3eA3Mzvc+jm+fDoPzXIQkPV18N79ejvOWnRpTlcf/XTVV4Aq+MU0uzZaZI12rZVml4YzVcQHDTrEYufBKuQaZEfX1TvDWhEv98+ty79dGX7HRx438ofQfB04Th08jNS+us+n+1l/XbfZKrgcumj/trRnpfak0Dw54Xp/nC+Xy5bOB7x6dDxt1sq5j/tP52vkd5Ee+Xc1JfnKmergxKdcOyOSCgLik5xXEtXBtXVVvTFcC+pdV6+YqIVT+rpNf4hKjqMgXdo9frO5ldAM1dFJfA7+1O9srqiM4W6otuYQIZ0pvivg8mWUvo6q14VImuTocf/JXFq4cP8lPifld/jXidQqOL0CX0V66L9a6Y9Pu34nc7XW8qoQQ9GfcghVwiq4kStqDdl1hGZpZ3f/ZnN9qyCHkTrWq4nl/l/8n8tVmieL1lpFhiDw0uQ84jeXl4ahp+tay/1r6/ai3/kchxKQI6njyCX64zg8n2s4RZEhIDkJ5ZD9b/mTzZnVq2mmMFP/RjJpwNObf7M5qa0Lj9K837/kvKuEu1ov6p9EiEXkd8ei+57f+VwJPSCSfSnNWkR8PvefytFvK/1riPiIEkXM1sGl39qrWlcEm9K3OXnXn2wO4qcFhWZ02uFk2dO/uTwMJx9ostn6Q4mq4LzvDmoSqXqzE5+8BHiOVsJ7arH661ZFuixCGp/+z+ZsWF+ROuR3I9faS39YBS82F9d2rpWkUzWchqFFdWitS5C+9F+5vC/3T/9Fkl8Y+H3BN/9mc/KHRwrxGa9iePnHGvhf9uWfM+/25Z8z7/Zv/gPV7u6J7fyCcQAAAABJRU5ErkJggg==",
|
||||
qrUrl: "http://localhost:8080/realms/master/account/totp?mode=qr",
|
||||
otpCredentials: []
|
||||
},
|
||||
url: {
|
||||
resourcesPath: "/resources/ueycc/account/keycloakify-starter",
|
||||
resourceUrl: "http://localhost:8080/realms/master/account/resource",
|
||||
resourcesCommonPath: "/resources/ueycc/account/keycloakify-starter/resources-common",
|
||||
logUrl: "http://localhost:8080/realms/master/account/log",
|
||||
socialUrl: "http://localhost:8080/realms/master/account/identity",
|
||||
accountUrl: "http://localhost:8080/realms/master/account/",
|
||||
sessionsUrl: "http://localhost:8080/realms/master/account/sessions",
|
||||
totpUrl: "http://localhost:8080/realms/master/account/totp",
|
||||
applicationsUrl: "http://localhost:8080/realms/master/account/applications",
|
||||
passwordUrl: "http://localhost:8080/realms/master/account/password"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithTotpEnabled = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "HB2W ESCK KJKF K5DC GJQX S5RQ I5AX CZ2U",
|
||||
totpSecret: "8ubHJRTUtb2ayv0GAqgT",
|
||||
manualUrl: "http://localhost:8080/realms/master/account/totp?mode=manual",
|
||||
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACoUlEQVR4Xu2YPW7DMAxGGWTImCP4JvbFAtiAL+bcxEfI6MEI+z3Kzl+BLh2oIRwEV08FJIr8SMX8T1vtc+bdvvxz5t2+/HPm3eA3Mzvc+jm+fDoPzXIQkPV18N79ejvOWnRpTlcf/XTVV4Aq+MU0uzZaZI12rZVml4YzVcQHDTrEYufBKuQaZEfX1TvDWhEv98+ty79dGX7HRx438ofQfB04Th08jNS+us+n+1l/XbfZKrgcumj/trRnpfak0Dw54Xp/nC+Xy5bOB7x6dDxt1sq5j/tP52vkd5Ee+Xc1JfnKmergxKdcOyOSCgLik5xXEtXBtXVVvTFcC+pdV6+YqIVT+rpNf4hKjqMgXdo9frO5ldAM1dFJfA7+1O9srqiM4W6otuYQIZ0pvivg8mWUvo6q14VImuTocf/JXFq4cP8lPifld/jXidQqOL0CX0V66L9a6Y9Pu34nc7XW8qoQQ9GfcghVwiq4kStqDdl1hGZpZ3f/ZnN9qyCHkTrWq4nl/l/8n8tVmieL1lpFhiDw0uQ84jeXl4ahp+tay/1r6/ai3/kchxKQI6njyCX64zg8n2s4RZEhIDkJ5ZD9b/mTzZnVq2mmMFP/RjJpwNObf7M5qa0Lj9K837/kvKuEu1ov6p9EiEXkd8ei+57f+VwJPSCSfSnNWkR8PvefytFvK/1riPiIEkXM1sGl39qrWlcEm9K3OXnXn2wO4qcFhWZ02uFk2dO/uTwMJx9ostn6Q4mq4LzvDmoSqXqzE5+8BHiOVsJ7arH661ZFuixCGp/+z+ZsWF+ROuR3I9faS39YBS82F9d2rpWkUzWchqFFdWitS5C+9F+5vC/3T/9Fkl8Y+H3BN/9mc/KHRwrxGa9iePnHGvhf9uWfM+/25Z8z7/Zv/gPV7u6J7fyCcQAAAABJRU5ErkJggg==",
|
||||
qrUrl: "http://localhost:8080/realms/master/account/totp?mode=qr",
|
||||
otpCredentials: []
|
||||
},
|
||||
url: {
|
||||
resourcesPath: "/resources/ueycc/account/keycloakify-starter",
|
||||
resourceUrl: "http://localhost:8080/realms/master/account/resource",
|
||||
resourcesCommonPath: "/resources/ueycc/account/keycloakify-starter/resources-common",
|
||||
logUrl: "http://localhost:8080/realms/master/account/log",
|
||||
socialUrl: "http://localhost:8080/realms/master/account/identity",
|
||||
accountUrl: "http://localhost:8080/realms/master/account/",
|
||||
sessionsUrl: "http://localhost:8080/realms/master/account/sessions",
|
||||
totpUrl: "http://localhost:8080/realms/master/account/totp",
|
||||
applicationsUrl: "http://localhost:8080/realms/master/account/applications",
|
||||
passwordUrl: "http://localhost:8080/realms/master/account/password"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithManualMode = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
mode: "manual",
|
||||
totp: {
|
||||
enabled: false,
|
||||
totpSecretEncoded: "HB2W ESCK KJKF K5DC GJQX S5RQ I5AX CZ2U",
|
||||
totpSecret: "8ubHJRTUtb2ayv0GAqgT",
|
||||
manualUrl: "http://localhost:8080/realms/master/account/totp?mode=manual",
|
||||
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACoUlEQVR4Xu2YPW7DMAxGGWTImCP4JvbFAtiAL+bcxEfI6MEI+z3Kzl+BLh2oIRwEV08FJIr8SMX8T1vtc+bdvvxz5t2+/HPm3eA3Mzvc+jm+fDoPzXIQkPV18N79ejvOWnRpTlcf/XTVV4Aq+MU0uzZaZI12rZVml4YzVcQHDTrEYufBKuQaZEfX1TvDWhEv98+ty79dGX7HRx438ofQfB04Th08jNS+us+n+1l/XbfZKrgcumj/trRnpfak0Dw54Xp/nC+Xy5bOB7x6dDxt1sq5j/tP52vkd5Ee+Xc1JfnKmergxKdcOyOSCgLik5xXEtXBtXVVvTFcC+pdV6+YqIVT+rpNf4hKjqMgXdo9frO5ldAM1dFJfA7+1O9srqiM4W6otuYQIZ0pvivg8mWUvo6q14VImuTocf/JXFq4cP8lPifld/jXidQqOL0CX0V66L9a6Y9Pu34nc7XW8qoQQ9GfcghVwiq4kStqDdl1hGZpZ3f/ZnN9qyCHkTrWq4nl/l/8n8tVmieL1lpFhiDw0uQ84jeXl4ahp+tay/1r6/ai3/kchxKQI6njyCX64zg8n2s4RZEhIDkJ5ZD9b/mTzZnVq2mmMFP/RjJpwNObf7M5qa0Lj9K837/kvKuEu1ov6p9EiEXkd8ei+57f+VwJPSCSfSnNWkR8PvefytFvK/1riPiIEkXM1sGl39qrWlcEm9K3OXnXn2wO4qcFhWZ02uFk2dO/uTwMJx9ostn6Q4mq4LzvDmoSqXqzE5+8BHiOVsJ7arH661ZFuixCGp/+z+ZsWF+ROuR3I9faS39YBS82F9d2rpWkUzWchqFFdWitS5C+9F+5vC/3T/9Fkl8Y+H3BN/9mc/KHRwrxGa9iePnHGvhf9uWfM+/25Z8z7/Zv/gPV7u6J7fyCcQAAAABJRU5ErkJggg==",
|
||||
qrUrl: "http://localhost:8080/realms/master/account/totp?mode=qr",
|
||||
otpCredentials: []
|
||||
},
|
||||
url: {
|
||||
resourcesPath: "/resources/ueycc/account/keycloakify-starter",
|
||||
resourceUrl: "http://localhost:8080/realms/master/account/resource",
|
||||
resourcesCommonPath: "/resources/ueycc/account/keycloakify-starter/resources-common",
|
||||
logUrl: "http://localhost:8080/realms/master/account/log",
|
||||
socialUrl: "http://localhost:8080/realms/master/account/identity",
|
||||
accountUrl: "http://localhost:8080/realms/master/account/",
|
||||
sessionsUrl: "http://localhost:8080/realms/master/account/sessions",
|
||||
totpUrl: "http://localhost:8080/realms/master/account/totp",
|
||||
applicationsUrl: "http://localhost:8080/realms/master/account/applications",
|
||||
passwordUrl: "http://localhost:8080/realms/master/account/password"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
@ -361,40 +361,6 @@ describe("js replacer - webpack", () => {
|
||||
|
||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||
});
|
||||
|
||||
it("replaceImportsInJsCode_webpack - 4", () => {
|
||||
const jsCodeUntransformed = `d={NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST`;
|
||||
|
||||
const { fixedJsCode } = replaceImportsInJsCode_webpack({
|
||||
"jsCode": jsCodeUntransformed,
|
||||
"buildOptions": {
|
||||
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
|
||||
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/build/static",
|
||||
"urlPathname": undefined
|
||||
}
|
||||
});
|
||||
|
||||
const fixedJsCodeExpected = `d={NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}",WDS_SOCKET_HOST`;
|
||||
|
||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||
});
|
||||
|
||||
it("replaceImportsInJsCode_webpack - 5", () => {
|
||||
const jsCodeUntransformed = `d={NODE_ENV:"production",PUBLIC_URL:"/foo-bar",WDS_SOCKET_HOST`;
|
||||
|
||||
const { fixedJsCode } = replaceImportsInJsCode_webpack({
|
||||
"jsCode": jsCodeUntransformed,
|
||||
"buildOptions": {
|
||||
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
|
||||
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/build/static",
|
||||
"urlPathname": "/foo-bar/"
|
||||
}
|
||||
});
|
||||
|
||||
const fixedJsCodeExpected = `d={NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}",WDS_SOCKET_HOST`;
|
||||
|
||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("css replacer", () => {
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
|
||||
import { downloadAndUnzip } from "keycloakify/bin/downloadAndUnzip";
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "keycloakify/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export async function setupSampleReactProject(destDirPath: string) {
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
await downloadAndUnzip({
|
||||
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||
"destDirPath": destDirPath,
|
||||
"doUseCache": false
|
||||
"buildOptions": {
|
||||
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
|
||||
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
|
||||
}
|
||||
});
|
||||
}
|
||||
|
121
yarn.lock
121
yarn.lock
@ -1665,7 +1665,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.15":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||
@ -4744,30 +4744,29 @@ check-error@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
|
||||
|
||||
cheerio-select@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
|
||||
integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
|
||||
cheerio-select-tmp@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
|
||||
integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-select "^5.1.0"
|
||||
css-what "^6.1.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
css-select "^3.1.2"
|
||||
css-what "^4.0.0"
|
||||
domelementtype "^2.1.0"
|
||||
domhandler "^4.0.0"
|
||||
domutils "^2.4.4"
|
||||
|
||||
cheerio@^1.0.0-rc.5:
|
||||
version "1.0.0-rc.12"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
|
||||
integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
|
||||
cheerio@1.0.0-rc.5:
|
||||
version "1.0.0-rc.5"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f"
|
||||
integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==
|
||||
dependencies:
|
||||
cheerio-select "^2.1.0"
|
||||
dom-serializer "^2.0.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
htmlparser2 "^8.0.1"
|
||||
parse5 "^7.0.0"
|
||||
parse5-htmlparser2-tree-adapter "^7.0.0"
|
||||
cheerio-select-tmp "^0.1.0"
|
||||
dom-serializer "~1.2.0"
|
||||
domhandler "^4.0.0"
|
||||
entities "~2.1.0"
|
||||
htmlparser2 "^6.0.0"
|
||||
parse5 "^6.0.0"
|
||||
parse5-htmlparser2-tree-adapter "^6.0.0"
|
||||
|
||||
chokidar@^2.1.8:
|
||||
version "2.1.8"
|
||||
@ -5349,6 +5348,17 @@ css-loader@^5.0.1:
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-select@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
|
||||
integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^4.0.0"
|
||||
domhandler "^4.0.0"
|
||||
domutils "^2.4.3"
|
||||
nth-check "^2.0.0"
|
||||
|
||||
css-select@^4.1.3:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
||||
@ -5360,18 +5370,12 @@ css-select@^4.1.3:
|
||||
domutils "^2.8.0"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
css-what@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
|
||||
integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
|
||||
|
||||
css-what@^6.0.1, css-what@^6.1.0:
|
||||
css-what@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
@ -5656,6 +5660,15 @@ dom-serializer@^2.0.0:
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
dom-serializer@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
|
||||
integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
|
||||
dependencies:
|
||||
domelementtype "^2.0.1"
|
||||
domhandler "^4.0.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
dom-walk@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
|
||||
@ -5666,7 +5679,7 @@ domain-browser@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
||||
|
||||
domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
domelementtype@^2.0.1, domelementtype@^2.1.0, domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
@ -5685,7 +5698,7 @@ domhandler@^5.0, domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^2.5.2, domutils@^2.8.0:
|
||||
domutils@^2.4.3, domutils@^2.4.4, domutils@^2.5.2, domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
|
||||
@ -5843,6 +5856,11 @@ entities@^4.2.0, entities@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
entities@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
|
||||
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
|
||||
|
||||
err-code@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
|
||||
@ -7206,7 +7224,7 @@ html-webpack-plugin@^5.0.0:
|
||||
pretty-error "^4.0.0"
|
||||
tapable "^2.0.0"
|
||||
|
||||
htmlparser2@^6.1.0:
|
||||
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
|
||||
@ -7216,7 +7234,7 @@ htmlparser2@^6.1.0:
|
||||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
htmlparser2@^8.0, htmlparser2@^8.0.1:
|
||||
htmlparser2@^8.0:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
@ -8352,6 +8370,13 @@ lz-string@^1.4.4:
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
magic-string@^0.30.7:
|
||||
version "0.30.7"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505"
|
||||
integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
|
||||
make-dir@^2.0.0, make-dir@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||
@ -9047,7 +9072,7 @@ npmlog@^5.0.1:
|
||||
gauge "^3.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
|
||||
nth-check@^2.0.1:
|
||||
nth-check@^2.0.0, nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
@ -9400,26 +9425,18 @@ parse-json@^5.0.0:
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
|
||||
integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
|
||||
parse5-htmlparser2-tree-adapter@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
|
||||
dependencies:
|
||||
domhandler "^5.0.2"
|
||||
parse5 "^7.0.0"
|
||||
parse5 "^6.0.1"
|
||||
|
||||
parse5@^6.0.0:
|
||||
parse5@^6.0.0, parse5@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
parse5@^7.0.0:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
|
||||
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
|
||||
dependencies:
|
||||
entities "^4.4.0"
|
||||
|
||||
parseurl@~1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
|
Reference in New Issue
Block a user