Compare commits
5 Commits
v8.1.1
...
v7.16.0-rc
Author | SHA1 | Date | |
---|---|---|---|
6256220a13 | |||
2a8d080681 | |||
721d654cb8 | |||
dfbb3886e7 | |||
3bb0377950 |
@ -158,16 +158,6 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "zavoloklom",
|
|
||||||
"name": "Sergey Kupletsky",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4151869?v=4",
|
|
||||||
"profile": "https://github.com/zavoloklom",
|
|
||||||
"contributions": [
|
|
||||||
"test",
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
@ -175,6 +165,5 @@
|
|||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"projectName": "keycloakify",
|
"projectName": "keycloakify",
|
||||||
"projectOwner": "keycloakify",
|
"projectOwner": "keycloakify"
|
||||||
"commitType": "docs"
|
|
||||||
}
|
}
|
||||||
|
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- v*
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@ -35,7 +34,7 @@ jobs:
|
|||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
#- run: yarn test:keycloakify-starter
|
- run: yarn test:keycloakify-starter
|
||||||
|
|
||||||
storybook:
|
storybook:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
74
README.md
74
README.md
@ -45,11 +45,9 @@
|
|||||||
> when using React; it's a well-regarded solution that many
|
> when using React; it's a well-regarded solution that many
|
||||||
> developers appreciate.
|
> developers appreciate.
|
||||||
|
|
||||||
> 📣 🛑 Account themes generated by Keycloakify are not currently compatible with Keycloak 22.
|
> 📣 🛑 Account themes generated by Keycloakify are currently not compatible with Keycloak 22.
|
||||||
> We are working on a solution. [Follow progress](https://github.com/keycloakify/keycloakify/issues/389).
|
> We are working on a solution. [More info](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1661591906).
|
||||||
> Login and email themes are not affected.
|
> Note that login and email themes are not affected.
|
||||||
> UPDATE: [The PR](https://github.com/keycloak/keycloak/pull/22317) that should future proof Keycloakify account themes has been greenlighted
|
|
||||||
> by the Keycloak team. Resolution is only a matter of time.
|
|
||||||
|
|
||||||
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
|
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
|
||||||
You can update your Keycloak, your Keycloakify generated theme won't break.
|
You can update your Keycloak, your Keycloakify generated theme won't break.
|
||||||
@ -116,7 +114,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -128,76 +125,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
|
|
||||||
# Changelog highlights
|
# Changelog highlights
|
||||||
|
|
||||||
## 8.0
|
|
||||||
|
|
||||||
- Much smaller .jar size. 70.2 MB -> 7.8 MB.
|
|
||||||
Keycloakify now detects which of the static resources from the default theme are actually used by your theme and only include those in the .jar.
|
|
||||||
- Build time: The first build is slowed but the subsequent build are faster. [Update your CI so that the cache is persisted across CI build](https://github.com/keycloakify/keycloakify-starter/commit/bc378d5afb67e796f520afbc348185f3e319d9d0).
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
There are very few breaking changes in this major version.
|
|
||||||
|
|
||||||
- The [`--external-assets` build option has been removed](https://docs.keycloakify.dev/v/v7/build-options#external-assets-deprecated) it was a performance optimization that is no longer relevant now that
|
|
||||||
we have lazy loading.
|
|
||||||
|
|
||||||
- `kcContext.usernameEditDisabled` is now `kcContext.usernameHidden`, the type was lying, it has been updated to reflect what's actually on the `kcContext` at runtime.
|
|
||||||
If you want to see in detail what should be updated [see issue](https://github.com/keycloakify/keycloakify/pull/399), or you can search and replace `usernameEditDisabled` -> `usernameHidden` it'll do the trick.
|
|
||||||
|
|
||||||
- The `usePrepareTemplate` prototype has been changed, you can search and replace:
|
|
||||||
|
|
||||||
`src/keycloak-theme/login/Template.tsx`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
url,
|
|
||||||
"stylesCommon": [
|
|
||||||
"node_modules/patternfly/dist/css/patternfly.min.css",
|
|
||||||
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
|
||||||
"lib/zocial/zocial.css"
|
|
||||||
],
|
|
||||||
"styles": ["css/login.css"],
|
|
||||||
```
|
|
||||||
|
|
||||||
by
|
|
||||||
|
|
||||||
```ts
|
|
||||||
"styles": [
|
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
|
||||||
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
|
|
||||||
`${url.resourcesPath}/css/login.css`
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
`src/keycloak-theme/account/Template.css`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
url,
|
|
||||||
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
|
||||||
"styles": ["css/account.css"],
|
|
||||||
```
|
|
||||||
|
|
||||||
by
|
|
||||||
|
|
||||||
```ts
|
|
||||||
"styles": [
|
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
|
||||||
`${url.resourcesPath}/css/account.css`
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7.15
|
## 7.15
|
||||||
|
|
||||||
- The i18n messages you defines in your theme are now also maid available to Keycloak.
|
- The i18n messages you defines in your theme are now also maid available to Keycloak.
|
||||||
In practice this mean that you can now customize the `kcContext.message.summary` that
|
In practice this mean that you can now customize the `kcContext.message.summary` that
|
||||||
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
|
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
|
||||||
are used to display specific error on some field of the form.
|
are used to display specific error on some field of the form.
|
||||||
[See video](https://youtu.be/D6tZcemReTI)
|
[See video](https://youtu.be/D6tZcemReTI)
|
||||||
|
|
||||||
## 7.14
|
## 7.14
|
||||||
|
|
||||||
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
|
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "8.1.1",
|
"version": "7.16.0-rc.1",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
|
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
|
||||||
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
|
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
|
||||||
"test": "yarn test:types && vitest run",
|
"test": "yarn test:types && vitest run",
|
||||||
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
|
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
|
||||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||||
|
@ -24,9 +24,9 @@ async function main() {
|
|||||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||||
|
|
||||||
await downloadBuiltinKeycloakTheme({
|
await downloadBuiltinKeycloakTheme({
|
||||||
"projectDirPath": getProjectRoot(),
|
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": tmpDirPath
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
type Dictionary = { [idiomId: string]: string };
|
||||||
|
@ -17,12 +17,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
|
|
||||||
const { isReady } = usePrepareTemplate({
|
const { isReady } = usePrepareTemplate({
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||||
"styles": [
|
url,
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
"styles": ["css/account.css"],
|
||||||
`${url.resourcesPath}/css/account.css`
|
"htmlClassName": undefined,
|
||||||
],
|
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
|
||||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,4 +11,4 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||||
|
@ -132,6 +132,10 @@ export const kcContextCommonMock: KcContext.Common = {
|
|||||||
],
|
],
|
||||||
"currentLanguageTag": "en"
|
"currentLanguageTag": "en"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "success",
|
||||||
|
"summary": "This is a test message"
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"authorization": true,
|
"authorization": true,
|
||||||
"identityFederation": true,
|
"identityFederation": true,
|
||||||
|
@ -3,7 +3,6 @@ import type { ClassKey } from "keycloakify/account/TemplateProps";
|
|||||||
|
|
||||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||||
"defaultClasses": {
|
"defaultClasses": {
|
||||||
"kcHtmlClass": undefined,
|
|
||||||
"kcBodyClass": undefined,
|
"kcBodyClass": undefined,
|
||||||
"kcButtonClass": "btn",
|
"kcButtonClass": "btn",
|
||||||
"kcButtonPrimaryClass": "btn-primary",
|
"kcButtonPrimaryClass": "btn-primary",
|
||||||
|
@ -24,11 +24,10 @@ import * as fs from "fs";
|
|||||||
|
|
||||||
for (const themeType of themeTypes) {
|
for (const themeType of themeTypes) {
|
||||||
await downloadKeycloakStaticResources({
|
await downloadKeycloakStaticResources({
|
||||||
projectDirPath,
|
"isSilent": false,
|
||||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||||
"themeType": themeType,
|
"themeType": themeType,
|
||||||
"themeDirPath": keycloakDirInPublicDir,
|
"themeDirPath": keycloakDirInPublicDir
|
||||||
"usedResources": undefined
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,72 +4,19 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
|||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
import { getLogger } from "./tools/logger";
|
import { getLogger } from "./tools/logger";
|
||||||
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||||
import * as child_process from "child_process";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: string; keycloakVersion: string; destDirPath: string }) {
|
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||||
const { projectDirPath, keycloakVersion, destDirPath } = params;
|
const { keycloakVersion, destDirPath } = params;
|
||||||
|
|
||||||
await downloadAndUnzip({
|
await Promise.all(
|
||||||
"doUseCache": true,
|
["", "-community"].map(ext =>
|
||||||
projectDirPath,
|
downloadAndUnzip({
|
||||||
destDirPath,
|
"destDirPath": destDirPath,
|
||||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||||
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
|
||||||
"preCacheTransform": {
|
})
|
||||||
"actionCacheId": "npm install and build",
|
)
|
||||||
"action": async ({ destDirPath }) => {
|
);
|
||||||
install_common_node_modules: {
|
|
||||||
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
|
|
||||||
|
|
||||||
if (!fs.existsSync(commonResourcesDirPath)) {
|
|
||||||
break install_common_node_modules;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
|
|
||||||
break install_common_node_modules;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(pathJoin(commonResourcesDirPath, "node_modules"))) {
|
|
||||||
break install_common_node_modules;
|
|
||||||
}
|
|
||||||
|
|
||||||
child_process.execSync("npm install --omit=dev", {
|
|
||||||
"cwd": commonResourcesDirPath,
|
|
||||||
"stdio": "ignore"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
child_process.execSync("npm 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("npm run check-types", "true")
|
|
||||||
.replace("npm run babel", "true");
|
|
||||||
|
|
||||||
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
|
|
||||||
|
|
||||||
child_process.execSync("npm run build", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
|
||||||
|
|
||||||
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
|
|
||||||
|
|
||||||
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -86,9 +33,9 @@ async function main() {
|
|||||||
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||||
|
|
||||||
await downloadBuiltinKeycloakTheme({
|
await downloadBuiltinKeycloakTheme({
|
||||||
"projectDirPath": process.cwd(),
|
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
destDirPath
|
destDirPath,
|
||||||
|
"isSilent": buildOptions.isSilent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,17 +10,15 @@ import { getLogger } from "./tools/logger";
|
|||||||
import { getThemeSrcDirPath } from "./getSrcDirPath";
|
import { getThemeSrcDirPath } from "./getSrcDirPath";
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const projectDirPath = process.cwd();
|
|
||||||
|
|
||||||
const { isSilent } = readBuildOptions({
|
const { isSilent } = readBuildOptions({
|
||||||
projectDirPath,
|
"projectDirPath": process.cwd(),
|
||||||
"processArgv": process.argv.slice(2)
|
"processArgv": process.argv.slice(2)
|
||||||
});
|
});
|
||||||
|
|
||||||
const logger = getLogger({ isSilent });
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
projectDirPath
|
"projectDirPath": process.cwd()
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||||
@ -36,9 +34,9 @@ export async function main() {
|
|||||||
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||||
|
|
||||||
await downloadBuiltinKeycloakTheme({
|
await downloadBuiltinKeycloakTheme({
|
||||||
projectDirPath,
|
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": builtinKeycloakThemeTmpDirPath
|
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
|
@ -4,135 +4,228 @@ import { parse as urlParse } from "url";
|
|||||||
import { typeGuard } from "tsafe/typeGuard";
|
import { typeGuard } from "tsafe/typeGuard";
|
||||||
import { symToStr } from "tsafe/symToStr";
|
import { symToStr } from "tsafe/symToStr";
|
||||||
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
|
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
|
||||||
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, sep as pathSep } from "path";
|
import { join as pathJoin, sep as pathSep } from "path";
|
||||||
import parseArgv from "minimist";
|
import parseArgv from "minimist";
|
||||||
|
|
||||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
export type BuildOptions = {
|
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||||
isSilent: boolean;
|
|
||||||
themeVersion: string;
|
export namespace BuildOptions {
|
||||||
themeName: string;
|
export type Common = {
|
||||||
extraThemeNames: string[];
|
isSilent: boolean;
|
||||||
extraThemeProperties: string[] | undefined;
|
themeVersion: string;
|
||||||
groupId: string;
|
themeName: string;
|
||||||
artifactId: string;
|
extraThemeNames: string[];
|
||||||
bundler: Bundler;
|
extraThemeProperties: string[] | undefined;
|
||||||
keycloakVersionDefaultAssets: string;
|
groupId: string;
|
||||||
/** Directory of your built react project. Defaults to {cwd}/build */
|
artifactId: string;
|
||||||
reactAppBuildDirPath: string;
|
bundler: Bundler;
|
||||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
keycloakVersionDefaultAssets: string;
|
||||||
keycloakifyBuildDirPath: string;
|
/** Directory of your built react project. Defaults to {cwd}/build */
|
||||||
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
|
reactAppBuildDirPath: string;
|
||||||
* In this case the urlPathname will be "/my-app/" */
|
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||||
urlPathname: string | undefined;
|
keycloakifyBuildDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
|
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
|
||||||
const { projectDirPath, processArgv } = params;
|
const { projectDirPath, processArgv } = params;
|
||||||
|
|
||||||
const { isSilentCliParamProvided } = (() => {
|
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
|
||||||
const argv = parseArgv(processArgv);
|
const argv = parseArgv(processArgv);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
|
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||||
|
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
|
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
|
||||||
|
|
||||||
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
const url = (() => {
|
||||||
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
|
let url: URL | undefined = undefined;
|
||||||
|
|
||||||
const themeName =
|
if (homepage !== undefined) {
|
||||||
keycloakify.themeName ??
|
url = new URL(homepage);
|
||||||
name
|
}
|
||||||
.replace(/^@(.*)/, "$1")
|
|
||||||
.split("/")
|
|
||||||
.join("-");
|
|
||||||
|
|
||||||
return {
|
const CNAME = (() => {
|
||||||
themeName,
|
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
|
||||||
extraThemeNames,
|
|
||||||
"bundler": (() => {
|
|
||||||
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
|
||||||
|
|
||||||
assert(
|
if (!fs.existsSync(cnameFilePath)) {
|
||||||
typeGuard<Bundler | undefined>(KEYCLOAKIFY_BUNDLER, [undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)),
|
|
||||||
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
|
||||||
})(),
|
|
||||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
|
||||||
"groupId": (() => {
|
|
||||||
const fallbackGroupId = `${themeName}.keycloak`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
|
||||||
groupId ??
|
|
||||||
(!homepage
|
|
||||||
? fallbackGroupId
|
|
||||||
: urlParse(homepage)
|
|
||||||
.host?.replace(/:[0-9]+$/, "")
|
|
||||||
?.split(".")
|
|
||||||
.reverse()
|
|
||||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
|
||||||
extraThemeProperties,
|
|
||||||
"isSilent": isSilentCliParamProvided,
|
|
||||||
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
|
||||||
"reactAppBuildDirPath": (() => {
|
|
||||||
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
|
||||||
|
|
||||||
if (reactAppBuildDirPath === undefined) {
|
|
||||||
return pathJoin(projectDirPath, "build");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathSep === "\\") {
|
|
||||||
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
|
||||||
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reactAppBuildDirPath;
|
|
||||||
})(),
|
|
||||||
"keycloakifyBuildDirPath": (() => {
|
|
||||||
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
|
||||||
|
|
||||||
if (keycloakifyBuildDirPath === undefined) {
|
|
||||||
return pathJoin(projectDirPath, "build_keycloak");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathSep === "\\") {
|
|
||||||
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
|
||||||
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keycloakifyBuildDirPath;
|
|
||||||
})(),
|
|
||||||
"urlPathname": (() => {
|
|
||||||
const { homepage } = parsedPackageJson;
|
|
||||||
|
|
||||||
let url: URL | undefined = undefined;
|
|
||||||
|
|
||||||
if (homepage !== undefined) {
|
|
||||||
url = new URL(homepage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url === undefined) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const out = url.pathname.replace(/([^/])$/, "$1/");
|
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||||
return out === "/" ? undefined : out;
|
})();
|
||||||
})()
|
|
||||||
};
|
if (CNAME !== undefined) {
|
||||||
|
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"origin": url.origin,
|
||||||
|
"pathname": (() => {
|
||||||
|
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||||
|
|
||||||
|
return out === "/" ? undefined : out;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const common: BuildOptions.Common = (() => {
|
||||||
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
|
||||||
|
|
||||||
|
const themeName =
|
||||||
|
keycloakify.themeName ??
|
||||||
|
name
|
||||||
|
.replace(/^@(.*)/, "$1")
|
||||||
|
.split("/")
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeName,
|
||||||
|
extraThemeNames,
|
||||||
|
"bundler": (() => {
|
||||||
|
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
typeGuard<Bundler | undefined>(
|
||||||
|
KEYCLOAKIFY_BUNDLER,
|
||||||
|
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
|
||||||
|
),
|
||||||
|
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||||
|
})(),
|
||||||
|
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||||
|
"groupId": (() => {
|
||||||
|
const fallbackGroupId = `${themeName}.keycloak`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||||
|
groupId ??
|
||||||
|
(!homepage
|
||||||
|
? fallbackGroupId
|
||||||
|
: urlParse(homepage)
|
||||||
|
.host?.replace(/:[0-9]+$/, "")
|
||||||
|
?.split(".")
|
||||||
|
.reverse()
|
||||||
|
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
||||||
|
extraThemeProperties,
|
||||||
|
"isSilent": isSilentCliParamProvided,
|
||||||
|
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
||||||
|
"reactAppBuildDirPath": (() => {
|
||||||
|
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||||
|
|
||||||
|
if (reactAppBuildDirPath === undefined) {
|
||||||
|
return pathJoin(projectDirPath, "build");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathSep === "\\") {
|
||||||
|
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||||
|
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactAppBuildDirPath;
|
||||||
|
})(),
|
||||||
|
"keycloakifyBuildDirPath": (() => {
|
||||||
|
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||||
|
|
||||||
|
if (keycloakifyBuildDirPath === undefined) {
|
||||||
|
return pathJoin(projectDirPath, "build_keycloak");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathSep === "\\") {
|
||||||
|
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||||
|
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keycloakifyBuildDirPath;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isExternalAssetsCliParamProvided) {
|
||||||
|
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return id<BuildOptions.ExternalAssets.SameDomain>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
url !== undefined,
|
||||||
|
[
|
||||||
|
"Can't compile in external assets mode if we don't know where",
|
||||||
|
"the app will be hosted.",
|
||||||
|
"You should provide a homepage field in the package.json (or create a",
|
||||||
|
"public/CNAME file.",
|
||||||
|
"Alternatively, if your app and the Keycloak server are on the same domain, ",
|
||||||
|
"eg https://example.com is your app and https://example.com/auth is the keycloak",
|
||||||
|
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
|
||||||
|
"in your package.json"
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
return id<BuildOptions.ExternalAssets.DifferentDomains>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": false,
|
||||||
|
"urlOrigin": url.origin,
|
||||||
|
"urlPathname": url.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<BuildOptions.Standalone>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": true,
|
||||||
|
"urlPathname": url?.pathname
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -484,15 +484,16 @@
|
|||||||
<#continue>
|
<#continue>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
<#-- 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())>
|
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||||
<#continue>
|
<#continue>
|
||||||
</#if>
|
</#if>
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
|
@ -13,11 +13,39 @@ export const themeTypes = ["login", "account"] as const;
|
|||||||
|
|
||||||
export type ThemeType = (typeof themeTypes)[number];
|
export type ThemeType = (typeof themeTypes)[number];
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
themeName: string;
|
|
||||||
themeVersion: string;
|
export namespace BuildOptionsLike {
|
||||||
urlPathname: string | undefined;
|
export type Common = {
|
||||||
};
|
themeName: string;
|
||||||
|
themeVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = Common &
|
||||||
|
CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = Common &
|
||||||
|
CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
@ -35,23 +63,22 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
const $ = cheerio.load(indexHtmlCode);
|
const $ = cheerio.load(indexHtmlCode);
|
||||||
|
|
||||||
fix_imports_statements: {
|
fix_imports_statements: {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
break fix_imports_statements;
|
||||||
|
}
|
||||||
|
|
||||||
$("script:not([src])").each((...[, element]) => {
|
$("script:not([src])").each((...[, element]) => {
|
||||||
const jsCode = $(element).html();
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": $(element).html()!,
|
||||||
assert(jsCode !== null);
|
buildOptions
|
||||||
|
});
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
|
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
$(element).text(fixedJsCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("style").each((...[, element]) => {
|
$("style").each((...[, element]) => {
|
||||||
const cssCode = $(element).html();
|
|
||||||
|
|
||||||
assert(cssCode !== null);
|
|
||||||
|
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
"cssCode": $(element).html()!,
|
||||||
buildOptions
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +100,9 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
|
|
||||||
$(element).attr(
|
$(element).attr(
|
||||||
attrName,
|
attrName,
|
||||||
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
buildOptions.isStandalone
|
||||||
|
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||||
|
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { BuildOptions } from "./BuildOptions";
|
|
||||||
import type { ThemeType } from "./generateFtl";
|
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
|
||||||
themeName: string;
|
|
||||||
extraThemeNames: string[];
|
|
||||||
groupId: string;
|
|
||||||
artifactId: string;
|
|
||||||
themeVersion: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
|
||||||
|
|
||||||
export function generateJavaStackFiles(params: {
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
|
||||||
implementedThemeTypes: Record<ThemeType | "email", boolean>;
|
|
||||||
buildOptions: BuildOptionsLike;
|
|
||||||
}): {
|
|
||||||
jarFilePath: string;
|
|
||||||
} {
|
|
||||||
const {
|
|
||||||
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
implementedThemeTypes
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
{
|
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
|
||||||
pomFileCode: string;
|
|
||||||
} {
|
|
||||||
const pomFileCode = [
|
|
||||||
`<?xml version="1.0"?>`,
|
|
||||||
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
|
||||||
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
|
||||||
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
|
||||||
` <modelVersion>4.0.0</modelVersion>`,
|
|
||||||
` <groupId>${groupId}</groupId>`,
|
|
||||||
` <artifactId>${artifactId}</artifactId>`,
|
|
||||||
` <version>${themeVersion}</version>`,
|
|
||||||
` <name>${artifactId}</name>`,
|
|
||||||
` <description />`,
|
|
||||||
`</project>`
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
return { pomFileCode };
|
|
||||||
})();
|
|
||||||
|
|
||||||
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(pathDirname(themeManifestFilePath));
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
themeManifestFilePath,
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
"themes": [themeName, ...extraThemeNames].map(themeName => ({
|
|
||||||
"name": themeName,
|
|
||||||
"types": Object.entries(implementedThemeTypes)
|
|
||||||
.filter(([, isImplemented]) => isImplemented)
|
|
||||||
.map(([themeType]) => themeType)
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public enum AccountPages {
|
||||||
|
ACCOUNT,
|
||||||
|
PASSWORD,
|
||||||
|
TOTP,
|
||||||
|
FEDERATED_IDENTITY,
|
||||||
|
LOG,
|
||||||
|
SESSIONS,
|
||||||
|
APPLICATIONS,
|
||||||
|
RESOURCES,
|
||||||
|
RESOURCE_DETAIL;
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.events.Event;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public interface AccountProvider extends Provider {
|
||||||
|
|
||||||
|
AccountProvider setUriInfo(UriInfo uriInfo);
|
||||||
|
|
||||||
|
AccountProvider setHttpHeaders(HttpHeaders httpHeaders);
|
||||||
|
|
||||||
|
Response createResponse(AccountPages page);
|
||||||
|
|
||||||
|
AccountProvider setError(Response.Status status, String message, Object... parameters);
|
||||||
|
|
||||||
|
AccountProvider setErrors(Response.Status status, List<FormMessage> messages);
|
||||||
|
|
||||||
|
AccountProvider setSuccess(String message, Object... parameters);
|
||||||
|
|
||||||
|
AccountProvider setWarning(String message, Object... parameters);
|
||||||
|
|
||||||
|
AccountProvider setUser(UserModel user);
|
||||||
|
|
||||||
|
AccountProvider setProfileFormData(MultivaluedMap<String, String> formData);
|
||||||
|
|
||||||
|
AccountProvider setRealm(RealmModel realm);
|
||||||
|
|
||||||
|
AccountProvider setReferrer(String[] referrer);
|
||||||
|
|
||||||
|
AccountProvider setEvents(List<Event> events);
|
||||||
|
|
||||||
|
AccountProvider setSessions(List<UserSessionModel> sessions);
|
||||||
|
|
||||||
|
AccountProvider setPasswordSet(boolean passwordSet);
|
||||||
|
|
||||||
|
AccountProvider setStateChecker(String stateChecker);
|
||||||
|
|
||||||
|
AccountProvider setIdTokenHint(String idTokenHint);
|
||||||
|
|
||||||
|
AccountProvider setFeatures(
|
||||||
|
boolean social,
|
||||||
|
boolean events,
|
||||||
|
boolean passwordUpdateSupported,
|
||||||
|
boolean authorizationSupported);
|
||||||
|
|
||||||
|
AccountProvider setAttribute(String key, String value);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public interface AccountProviderFactory extends ProviderFactory<AccountProvider> {}
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account;
|
||||||
|
|
||||||
|
import com.google.auto.service.AutoService;
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
import org.keycloak.provider.Spi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
@AutoService(Spi.class)
|
||||||
|
public class AccountSpi implements Spi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInternal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "account";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Provider> getProviderClass() {
|
||||||
|
return AccountProvider.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||||
|
return AccountProviderFactory.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,424 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.forms.account.freemarker;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.events.Event;
|
||||||
|
import org.keycloak.forms.account.AccountPages;
|
||||||
|
import org.keycloak.forms.account.AccountProvider;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.AccountBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.AccountFederatedIdentityBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.ApplicationsBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.FeaturesBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.LogBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.PasswordBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.RealmBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.ReferrerBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.SessionsBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.TotpBean;
|
||||||
|
import org.keycloak.forms.account.freemarker.model.UrlBean;
|
||||||
|
import org.keycloak.forms.login.MessageType;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
|
import org.keycloak.theme.FreeMarkerException;
|
||||||
|
import org.keycloak.theme.Theme;
|
||||||
|
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
||||||
|
import org.keycloak.theme.beans.LocaleBean;
|
||||||
|
import org.keycloak.theme.beans.MessageBean;
|
||||||
|
import org.keycloak.theme.beans.MessageFormatterMethod;
|
||||||
|
import org.keycloak.theme.beans.MessagesPerFieldBean;
|
||||||
|
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||||
|
import org.keycloak.utils.MediaType;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class);
|
||||||
|
|
||||||
|
protected UserModel user;
|
||||||
|
protected MultivaluedMap<String, String> profileFormData;
|
||||||
|
protected Response.Status status = Response.Status.OK;
|
||||||
|
protected RealmModel realm;
|
||||||
|
protected String[] referrer;
|
||||||
|
protected List<Event> events;
|
||||||
|
protected String stateChecker;
|
||||||
|
protected String idTokenHint;
|
||||||
|
protected List<UserSessionModel> sessions;
|
||||||
|
protected boolean identityProviderEnabled;
|
||||||
|
protected boolean eventsEnabled;
|
||||||
|
protected boolean passwordUpdateSupported;
|
||||||
|
protected boolean passwordSet;
|
||||||
|
protected KeycloakSession session;
|
||||||
|
protected FreeMarkerProvider freeMarker;
|
||||||
|
protected HttpHeaders headers;
|
||||||
|
protected Map<String, Object> attributes;
|
||||||
|
|
||||||
|
protected UriInfo uriInfo;
|
||||||
|
|
||||||
|
protected List<FormMessage> messages = null;
|
||||||
|
protected MessageType messageType = MessageType.ERROR;
|
||||||
|
private boolean authorizationSupported;
|
||||||
|
|
||||||
|
public FreeMarkerAccountProvider(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
this.freeMarker = session.getProvider(FreeMarkerProvider.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountProvider setUriInfo(UriInfo uriInfo) {
|
||||||
|
this.uriInfo = uriInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setHttpHeaders(HttpHeaders httpHeaders) {
|
||||||
|
this.headers = httpHeaders;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response createResponse(AccountPages page) {
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
if (this.attributes != null) {
|
||||||
|
attributes.putAll(this.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme theme;
|
||||||
|
try {
|
||||||
|
theme = getTheme();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to create theme", e);
|
||||||
|
return Response.serverError().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Locale locale = session.getContext().resolveLocale(user);
|
||||||
|
Properties messagesBundle = handleThemeResources(theme, locale, attributes);
|
||||||
|
|
||||||
|
URI baseUri = uriInfo.getBaseUri();
|
||||||
|
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
||||||
|
for (Map.Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
|
||||||
|
baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray());
|
||||||
|
}
|
||||||
|
URI baseQueryUri = baseUriBuilder.build();
|
||||||
|
|
||||||
|
if (stateChecker != null) {
|
||||||
|
attributes.put("stateChecker", stateChecker);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessages(locale, messagesBundle, attributes);
|
||||||
|
|
||||||
|
if (referrer != null) {
|
||||||
|
attributes.put("referrer", new ReferrerBean(referrer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realm != null) {
|
||||||
|
attributes.put("realm", new RealmBean(realm));
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put(
|
||||||
|
"url",
|
||||||
|
new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), idTokenHint));
|
||||||
|
|
||||||
|
if (realm.isInternationalizationEnabled()) {
|
||||||
|
UriBuilder b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
|
||||||
|
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put(
|
||||||
|
"features",
|
||||||
|
new FeaturesBean(
|
||||||
|
identityProviderEnabled,
|
||||||
|
eventsEnabled,
|
||||||
|
passwordUpdateSupported,
|
||||||
|
authorizationSupported));
|
||||||
|
attributes.put("account", new AccountBean(user, profileFormData));
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
case TOTP:
|
||||||
|
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
|
||||||
|
break;
|
||||||
|
case FEDERATED_IDENTITY:
|
||||||
|
attributes.put(
|
||||||
|
"federatedIdentity",
|
||||||
|
new AccountFederatedIdentityBean(
|
||||||
|
session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
||||||
|
break;
|
||||||
|
case LOG:
|
||||||
|
attributes.put("log", new LogBean(events));
|
||||||
|
break;
|
||||||
|
case SESSIONS:
|
||||||
|
attributes.put("sessions", new SessionsBean(realm, sessions));
|
||||||
|
break;
|
||||||
|
case APPLICATIONS:
|
||||||
|
attributes.put("applications", new ApplicationsBean(session, realm, user));
|
||||||
|
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
|
||||||
|
break;
|
||||||
|
case PASSWORD:
|
||||||
|
attributes.put("password", new PasswordBean(passwordSet));
|
||||||
|
break;
|
||||||
|
case RESOURCES:
|
||||||
|
if (!realm.isUserManagedAccessAllowed()) {
|
||||||
|
return Response.status(Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
|
||||||
|
case RESOURCE_DETAIL:
|
||||||
|
if (!realm.isUserManagedAccessAllowed()) {
|
||||||
|
return Response.status(Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return processTemplate(theme, page, attributes, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Theme used for page rendering.
|
||||||
|
*
|
||||||
|
* @return theme for page rendering, never null
|
||||||
|
* @throws IOException in case of Theme loading problem
|
||||||
|
*/
|
||||||
|
protected Theme getTheme() throws IOException {
|
||||||
|
return session.theme().getTheme(Theme.Type.ACCOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load message bundle and place it into <code>msg</code> template attribute. Also load Theme
|
||||||
|
* properties and place them into <code>properties</code> template attribute.
|
||||||
|
*
|
||||||
|
* @param theme actual Theme to load bundle from
|
||||||
|
* @param locale to load bundle for
|
||||||
|
* @param attributes template attributes to add resources to
|
||||||
|
* @return message bundle for other use
|
||||||
|
*/
|
||||||
|
protected Properties handleThemeResources(
|
||||||
|
Theme theme, Locale locale, Map<String, Object> attributes) {
|
||||||
|
Properties messagesBundle = new Properties();
|
||||||
|
try {
|
||||||
|
if (!StringUtil.isNotBlank(realm.getDefaultLocale())) {
|
||||||
|
messagesBundle.putAll(realm.getRealmLocalizationTextsByLocale(realm.getDefaultLocale()));
|
||||||
|
}
|
||||||
|
messagesBundle.putAll(theme.getMessages(locale));
|
||||||
|
messagesBundle.putAll(realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag()));
|
||||||
|
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to load messages", e);
|
||||||
|
messagesBundle = new Properties();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
attributes.put("properties", theme.getProperties());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to load properties", e);
|
||||||
|
}
|
||||||
|
return messagesBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle messages to be shown on the page - set them to template attributes
|
||||||
|
*
|
||||||
|
* @param locale to be used for message text loading
|
||||||
|
* @param messagesBundle to be used for message text loading
|
||||||
|
* @param attributes template attributes to messages related info to
|
||||||
|
* @see #messageType
|
||||||
|
* @see #messages
|
||||||
|
*/
|
||||||
|
protected void handleMessages(
|
||||||
|
Locale locale, Properties messagesBundle, Map<String, Object> attributes) {
|
||||||
|
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
|
||||||
|
if (messages != null) {
|
||||||
|
MessageBean wholeMessage = new MessageBean(null, messageType);
|
||||||
|
for (FormMessage message : this.messages) {
|
||||||
|
String formattedMessageText = formatMessage(message, messagesBundle, locale);
|
||||||
|
if (formattedMessageText != null) {
|
||||||
|
wholeMessage.appendSummaryLine(formattedMessageText);
|
||||||
|
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributes.put("message", wholeMessage);
|
||||||
|
}
|
||||||
|
attributes.put("messagesPerField", messagesPerField);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process FreeMarker template and prepare Response. Some fields are used for rendering also.
|
||||||
|
*
|
||||||
|
* @param theme to be used (provided by <code>getTheme()</code>)
|
||||||
|
* @param page to be rendered
|
||||||
|
* @param attributes pushed to the template
|
||||||
|
* @param locale to be used
|
||||||
|
* @return Response object to be returned to the browser, never null
|
||||||
|
*/
|
||||||
|
protected Response processTemplate(
|
||||||
|
Theme theme, AccountPages page, Map<String, Object> attributes, Locale locale) {
|
||||||
|
try {
|
||||||
|
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
|
||||||
|
Response.ResponseBuilder builder =
|
||||||
|
Response.status(status)
|
||||||
|
.type(MediaType.TEXT_HTML_UTF_8_TYPE)
|
||||||
|
.language(locale)
|
||||||
|
.entity(result);
|
||||||
|
builder.cacheControl(CacheControlUtil.noCache());
|
||||||
|
return builder.build();
|
||||||
|
} catch (FreeMarkerException e) {
|
||||||
|
logger.error("Failed to process template", e);
|
||||||
|
return Response.serverError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountProvider setPasswordSet(boolean passwordSet) {
|
||||||
|
this.passwordSet = passwordSet;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setMessage(MessageType type, String message, Object... parameters) {
|
||||||
|
messageType = type;
|
||||||
|
messages = new ArrayList<>();
|
||||||
|
messages.add(new FormMessage(null, message, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
|
||||||
|
if (message == null) return null;
|
||||||
|
if (messagesBundle.containsKey(message.getMessage())) {
|
||||||
|
return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale)
|
||||||
|
.format(message.getParameters());
|
||||||
|
} else {
|
||||||
|
return message.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setErrors(Response.Status status, List<FormMessage> messages) {
|
||||||
|
this.status = status;
|
||||||
|
this.messageType = MessageType.ERROR;
|
||||||
|
this.messages = new ArrayList<>(messages);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setError(Response.Status status, String message, Object... parameters) {
|
||||||
|
this.status = status;
|
||||||
|
setMessage(MessageType.ERROR, message, parameters);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setSuccess(String message, Object... parameters) {
|
||||||
|
setMessage(MessageType.SUCCESS, message, parameters);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setWarning(String message, Object... parameters) {
|
||||||
|
setMessage(MessageType.WARNING, message, parameters);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setUser(UserModel user) {
|
||||||
|
this.user = user;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setProfileFormData(MultivaluedMap<String, String> formData) {
|
||||||
|
this.profileFormData = formData;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setRealm(RealmModel realm) {
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setReferrer(String[] referrer) {
|
||||||
|
this.referrer = referrer;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setEvents(List<Event> events) {
|
||||||
|
this.events = events;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setSessions(List<UserSessionModel> sessions) {
|
||||||
|
this.sessions = sessions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setStateChecker(String stateChecker) {
|
||||||
|
this.stateChecker = stateChecker;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setIdTokenHint(String idTokenHint) {
|
||||||
|
this.idTokenHint = idTokenHint;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setFeatures(
|
||||||
|
boolean identityProviderEnabled,
|
||||||
|
boolean eventsEnabled,
|
||||||
|
boolean passwordUpdateSupported,
|
||||||
|
boolean authorizationSupported) {
|
||||||
|
this.identityProviderEnabled = identityProviderEnabled;
|
||||||
|
this.eventsEnabled = eventsEnabled;
|
||||||
|
this.passwordUpdateSupported = passwordUpdateSupported;
|
||||||
|
this.authorizationSupported = authorizationSupported;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setAttribute(String key, String value) {
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new HashMap<>();
|
||||||
|
}
|
||||||
|
attributes.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker;
|
||||||
|
|
||||||
|
import com.google.auto.service.AutoService;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.forms.account.AccountProvider;
|
||||||
|
import org.keycloak.forms.account.AccountProviderFactory;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
@AutoService(AccountProviderFactory.class)
|
||||||
|
public class FreeMarkerAccountProviderFactory implements AccountProviderFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider create(KeycloakSession session) {
|
||||||
|
return new FreeMarkerAccountProvider(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "freemarker";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker;
|
||||||
|
|
||||||
|
import org.keycloak.forms.account.AccountPages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class Templates {
|
||||||
|
|
||||||
|
public static String getTemplate(AccountPages page) {
|
||||||
|
switch (page) {
|
||||||
|
case ACCOUNT:
|
||||||
|
return "account.ftl";
|
||||||
|
case PASSWORD:
|
||||||
|
return "password.ftl";
|
||||||
|
case TOTP:
|
||||||
|
return "totp.ftl";
|
||||||
|
case FEDERATED_IDENTITY:
|
||||||
|
return "federatedIdentity.ftl";
|
||||||
|
case LOG:
|
||||||
|
return "log.ftl";
|
||||||
|
case SESSIONS:
|
||||||
|
return "sessions.ftl";
|
||||||
|
case APPLICATIONS:
|
||||||
|
return "applications.ftl";
|
||||||
|
case RESOURCES:
|
||||||
|
return "resources.ftl";
|
||||||
|
case RESOURCE_DETAIL:
|
||||||
|
return "resource-detail.ftl";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class AccountBean {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(AccountBean.class);
|
||||||
|
|
||||||
|
private final UserModel user;
|
||||||
|
private final MultivaluedMap<String, String> profileFormData;
|
||||||
|
|
||||||
|
// TODO: More proper multi-value attribute support
|
||||||
|
private final Map<String, String> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
|
||||||
|
this.user = user;
|
||||||
|
this.profileFormData = profileFormData;
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
|
||||||
|
List<String> attrValue = attr.getValue();
|
||||||
|
if (attrValue.size() > 0) {
|
||||||
|
attributes.put(attr.getKey(), attrValue.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrValue.size() > 1) {
|
||||||
|
logger.warnf(
|
||||||
|
"There are more values for attribute '%s' of user '%s' . Will display just first value",
|
||||||
|
attr.getKey(), user.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileFormData != null) {
|
||||||
|
for (String key : profileFormData.keySet()) {
|
||||||
|
if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
|
||||||
|
String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||||
|
attributes.put(attribute, profileFormData.getFirst(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return profileFormData != null ? profileFormData.getFirst("firstName") : user.getFirstName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return profileFormData != null ? profileFormData.getFirst("lastName") : user.getLastName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
if (profileFormData != null && profileFormData.containsKey("username")) {
|
||||||
|
return profileFormData.getFirst("username");
|
||||||
|
} else {
|
||||||
|
return user.getUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return profileFormData != null ? profileFormData.getFirst("email") : user.getEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAttributes() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrderedModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.services.resources.account.AccountFormService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
* @author <a href="mailto:velias@redhat.com">Vlastimil Elias</a>
|
||||||
|
*/
|
||||||
|
public class AccountFederatedIdentityBean {
|
||||||
|
|
||||||
|
private static OrderedModel.OrderedModelComparator<FederatedIdentityEntry>
|
||||||
|
IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
|
||||||
|
|
||||||
|
private final List<FederatedIdentityEntry> identities;
|
||||||
|
private final boolean removeLinkPossible;
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public AccountFederatedIdentityBean(
|
||||||
|
KeycloakSession session, RealmModel realm, UserModel user, URI baseUri, String stateChecker) {
|
||||||
|
this.session = session;
|
||||||
|
|
||||||
|
AtomicInteger availableIdentities = new AtomicInteger(0);
|
||||||
|
this.identities =
|
||||||
|
realm
|
||||||
|
.getIdentityProvidersStream()
|
||||||
|
.filter(IdentityProviderModel::isEnabled)
|
||||||
|
.map(
|
||||||
|
provider -> {
|
||||||
|
String providerId = provider.getAlias();
|
||||||
|
|
||||||
|
FederatedIdentityModel identity =
|
||||||
|
getIdentity(
|
||||||
|
session.users().getFederatedIdentitiesStream(realm, user), providerId);
|
||||||
|
|
||||||
|
if (identity != null) {
|
||||||
|
availableIdentities.getAndIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
String displayName =
|
||||||
|
KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
|
||||||
|
return new FederatedIdentityEntry(
|
||||||
|
identity,
|
||||||
|
displayName,
|
||||||
|
provider.getAlias(),
|
||||||
|
provider.getAlias(),
|
||||||
|
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
|
||||||
|
})
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Removing last social provider is not possible if you don't have other possibility to
|
||||||
|
// authenticate
|
||||||
|
this.removeLinkPossible =
|
||||||
|
availableIdentities.get() > 1
|
||||||
|
|| user.getFederationLink() != null
|
||||||
|
|| AccountFormService.isPasswordSet(session, realm, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FederatedIdentityModel getIdentity(
|
||||||
|
Stream<FederatedIdentityModel> identities, String providerId) {
|
||||||
|
return identities
|
||||||
|
.filter(
|
||||||
|
federatedIdentityModel ->
|
||||||
|
Objects.equals(federatedIdentityModel.getIdentityProvider(), providerId))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FederatedIdentityEntry> getIdentities() {
|
||||||
|
return identities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemoveLinkPossible() {
|
||||||
|
return removeLinkPossible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FederatedIdentityEntry implements OrderedModel {
|
||||||
|
|
||||||
|
private FederatedIdentityModel federatedIdentityModel;
|
||||||
|
private final String providerId;
|
||||||
|
private final String providerName;
|
||||||
|
private final String guiOrder;
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
public FederatedIdentityEntry(
|
||||||
|
FederatedIdentityModel federatedIdentityModel,
|
||||||
|
String displayName,
|
||||||
|
String providerId,
|
||||||
|
String providerName,
|
||||||
|
String guiOrder) {
|
||||||
|
this.federatedIdentityModel = federatedIdentityModel;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.providerId = providerId;
|
||||||
|
this.providerName = providerName;
|
||||||
|
this.guiOrder = guiOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProviderId() {
|
||||||
|
return providerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProviderName() {
|
||||||
|
return providerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return federatedIdentityModel != null ? federatedIdentityModel.getUserId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return federatedIdentityModel != null ? federatedIdentityModel.getUserName() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return federatedIdentityModel != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGuiOrder() {
|
||||||
|
return guiOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrderedModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.UserConsentModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
|
import org.keycloak.storage.StorageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ApplicationsBean {
|
||||||
|
|
||||||
|
private List<ApplicationEntry> applications = new LinkedList<>();
|
||||||
|
|
||||||
|
public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
Set<ClientModel> offlineClients =
|
||||||
|
new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
|
||||||
|
|
||||||
|
this.applications =
|
||||||
|
this.getApplications(session, realm, user)
|
||||||
|
.filter(
|
||||||
|
client ->
|
||||||
|
!isAdminClient(client)
|
||||||
|
|| AdminPermissions.realms(session, realm, user).isAdmin())
|
||||||
|
.map(client -> toApplicationEntry(session, realm, user, client, offlineClients))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAdminClient(ClientModel client) {
|
||||||
|
return client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
|
||||||
|
|| client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<ClientModel> getApplications(
|
||||||
|
KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
Predicate<ClientModel> bearerOnly = ClientModel::isBearerOnly;
|
||||||
|
Stream<ClientModel> clients = realm.getClientsStream().filter(bearerOnly.negate());
|
||||||
|
|
||||||
|
Predicate<ClientModel> isLocal = client -> new StorageId(client.getId()).isLocal();
|
||||||
|
return Stream.concat(
|
||||||
|
clients,
|
||||||
|
session
|
||||||
|
.users()
|
||||||
|
.getConsentsStream(realm, user.getId())
|
||||||
|
.map(UserConsentModel::getClient)
|
||||||
|
.filter(isLocal.negate()))
|
||||||
|
.distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRoles(
|
||||||
|
Set<RoleModel> inputRoles,
|
||||||
|
List<RoleModel> realmRoles,
|
||||||
|
MultivaluedHashMap<String, ClientRoleEntry> clientRoles) {
|
||||||
|
for (RoleModel role : inputRoles) {
|
||||||
|
if (role.getContainer() instanceof RealmModel) {
|
||||||
|
realmRoles.add(role);
|
||||||
|
} else {
|
||||||
|
ClientModel currentClient = (ClientModel) role.getContainer();
|
||||||
|
ClientRoleEntry clientRole =
|
||||||
|
new ClientRoleEntry(
|
||||||
|
currentClient.getClientId(),
|
||||||
|
currentClient.getName(),
|
||||||
|
role.getName(),
|
||||||
|
role.getDescription());
|
||||||
|
clientRoles.add(currentClient.getClientId(), clientRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ApplicationEntry> getApplications() {
|
||||||
|
return applications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApplicationEntry {
|
||||||
|
|
||||||
|
private KeycloakSession session;
|
||||||
|
private final List<RoleModel> realmRolesAvailable;
|
||||||
|
private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable;
|
||||||
|
private final ClientModel client;
|
||||||
|
private final List<String> clientScopesGranted;
|
||||||
|
private final List<String> additionalGrants;
|
||||||
|
|
||||||
|
public ApplicationEntry(
|
||||||
|
KeycloakSession session,
|
||||||
|
List<RoleModel> realmRolesAvailable,
|
||||||
|
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable,
|
||||||
|
ClientModel client,
|
||||||
|
List<String> clientScopesGranted,
|
||||||
|
List<String> additionalGrants) {
|
||||||
|
this.session = session;
|
||||||
|
this.realmRolesAvailable = realmRolesAvailable;
|
||||||
|
this.resourceRolesAvailable = resourceRolesAvailable;
|
||||||
|
this.client = client;
|
||||||
|
this.clientScopesGranted = clientScopesGranted;
|
||||||
|
this.additionalGrants = additionalGrants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RoleModel> getRealmRolesAvailable() {
|
||||||
|
return realmRolesAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultivaluedHashMap<String, ClientRoleEntry> getResourceRolesAvailable() {
|
||||||
|
return resourceRolesAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getClientScopesGranted() {
|
||||||
|
return clientScopesGranted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEffectiveUrl() {
|
||||||
|
return ResolveRelative.resolveRelativeUri(
|
||||||
|
session, getClient().getRootUrl(), getClient().getBaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientModel getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAdditionalGrants() {
|
||||||
|
return additionalGrants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker...
|
||||||
|
public static class ClientRoleEntry {
|
||||||
|
|
||||||
|
private final String clientId;
|
||||||
|
private final String clientName;
|
||||||
|
private final String roleName;
|
||||||
|
private final String roleDescription;
|
||||||
|
|
||||||
|
public ClientRoleEntry(
|
||||||
|
String clientId, String clientName, String roleName, String roleDescription) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.clientName = clientName;
|
||||||
|
this.roleName = roleName;
|
||||||
|
this.roleDescription = roleDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientName() {
|
||||||
|
return clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoleName() {
|
||||||
|
return roleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoleDescription() {
|
||||||
|
return roleDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@link ApplicationEntry} from the specified parameters.
|
||||||
|
*
|
||||||
|
* @param session a reference to the {@code Keycloak} session.
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param user a reference to the user.
|
||||||
|
* @param client a reference to the client that contains the applications.
|
||||||
|
* @param offlineClients a {@link Set} containing the offline clients.
|
||||||
|
* @return the constructed {@link ApplicationEntry} instance or {@code null} if the user can't
|
||||||
|
* access the applications in the specified client.
|
||||||
|
*/
|
||||||
|
private ApplicationEntry toApplicationEntry(
|
||||||
|
final KeycloakSession session,
|
||||||
|
final RealmModel realm,
|
||||||
|
final UserModel user,
|
||||||
|
final ClientModel client,
|
||||||
|
final Set<ClientModel> offlineClients) {
|
||||||
|
|
||||||
|
// Construct scope parameter with all optional scopes to see all potentially available roles
|
||||||
|
Stream<ClientScopeModel> allClientScopes =
|
||||||
|
Stream.concat(
|
||||||
|
client.getClientScopes(true).values().stream(),
|
||||||
|
client.getClientScopes(false).values().stream());
|
||||||
|
allClientScopes = Stream.concat(allClientScopes, Stream.of(client)).distinct();
|
||||||
|
|
||||||
|
Set<RoleModel> availableRoles = TokenManager.getAccess(user, client, allClientScopes);
|
||||||
|
|
||||||
|
// Don't show applications, which user doesn't have access into (any available roles)
|
||||||
|
// unless this is can be changed by approving/revoking consent
|
||||||
|
if (!isAdminClient(client) && availableRoles.isEmpty() && !client.isConsentRequired()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RoleModel> realmRolesAvailable = new LinkedList<>();
|
||||||
|
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable = new MultivaluedHashMap<>();
|
||||||
|
processRoles(availableRoles, realmRolesAvailable, resourceRolesAvailable);
|
||||||
|
|
||||||
|
List<ClientScopeModel> orderedScopes = new LinkedList<>();
|
||||||
|
if (client.isConsentRequired()) {
|
||||||
|
UserConsentModel consent =
|
||||||
|
session.users().getConsentByClient(realm, user.getId(), client.getId());
|
||||||
|
|
||||||
|
if (consent != null) {
|
||||||
|
orderedScopes.addAll(consent.getGrantedClientScopes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> clientScopesGranted =
|
||||||
|
orderedScopes.stream()
|
||||||
|
.sorted(OrderedModel.OrderedModelComparator.getInstance())
|
||||||
|
.map(ClientScopeModel::getConsentScreenText)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> additionalGrants = new ArrayList<>();
|
||||||
|
if (offlineClients.contains(client)) {
|
||||||
|
additionalGrants.add("${offlineToken}");
|
||||||
|
}
|
||||||
|
return new ApplicationEntry(
|
||||||
|
session,
|
||||||
|
realmRolesAvailable,
|
||||||
|
resourceRolesAvailable,
|
||||||
|
client,
|
||||||
|
clientScopesGranted,
|
||||||
|
additionalGrants);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,515 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
import org.keycloak.authorization.model.PermissionTicket;
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.model.Resource;
|
||||||
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
|
import org.keycloak.authorization.model.Scope;
|
||||||
|
import org.keycloak.authorization.store.PermissionTicketStore;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||||
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationBean {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final UserModel user;
|
||||||
|
private final AuthorizationProvider authorization;
|
||||||
|
private final UriInfo uriInfo;
|
||||||
|
private ResourceBean resource;
|
||||||
|
private List<ResourceBean> resources;
|
||||||
|
private Collection<ResourceBean> userSharedResources;
|
||||||
|
private Collection<ResourceBean> requestsWaitingPermission;
|
||||||
|
private Collection<ResourceBean> resourcesWaitingOthersApproval;
|
||||||
|
|
||||||
|
public AuthorizationBean(
|
||||||
|
KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) {
|
||||||
|
this.session = session;
|
||||||
|
this.realm = realm;
|
||||||
|
this.user = user;
|
||||||
|
this.uriInfo = uriInfo;
|
||||||
|
authorization = session.getProvider(AuthorizationProvider.class);
|
||||||
|
List<String> pathParameters = uriInfo.getPathParameters().get("resource_id");
|
||||||
|
|
||||||
|
if (pathParameters != null && !pathParameters.isEmpty()) {
|
||||||
|
Resource resource =
|
||||||
|
authorization
|
||||||
|
.getStoreFactory()
|
||||||
|
.getResourceStore()
|
||||||
|
.findById(realm, null, pathParameters.get(0));
|
||||||
|
|
||||||
|
if (resource != null && !resource.getOwner().equals(user.getId())) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"User [" + user.getUsername() + "] can not access resource [" + resource.getId() + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ResourceBean> getResourcesWaitingOthersApproval() {
|
||||||
|
if (resourcesWaitingOthersApproval == null) {
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters =
|
||||||
|
new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||||
|
|
||||||
|
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
|
||||||
|
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
|
||||||
|
|
||||||
|
resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourcesWaitingOthersApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ResourceBean> getResourcesWaitingApproval() {
|
||||||
|
if (requestsWaitingPermission == null) {
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters =
|
||||||
|
new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||||
|
|
||||||
|
filters.put(PermissionTicket.FilterOption.OWNER, user.getId());
|
||||||
|
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
|
||||||
|
|
||||||
|
requestsWaitingPermission = toResourceRepresentation(findPermissions(filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestsWaitingPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ResourceBean> getResources() {
|
||||||
|
if (resources == null) {
|
||||||
|
resources =
|
||||||
|
authorization
|
||||||
|
.getStoreFactory()
|
||||||
|
.getResourceStore()
|
||||||
|
.findByOwner(realm, null, user.getId())
|
||||||
|
.stream()
|
||||||
|
.filter(Resource::isOwnerManagedAccess)
|
||||||
|
.map(ResourceBean::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ResourceBean> getSharedResources() {
|
||||||
|
if (userSharedResources == null) {
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters =
|
||||||
|
new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||||
|
|
||||||
|
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
|
||||||
|
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
|
||||||
|
|
||||||
|
PermissionTicketStore ticketStore =
|
||||||
|
authorization.getStoreFactory().getPermissionTicketStore();
|
||||||
|
|
||||||
|
userSharedResources =
|
||||||
|
toResourceRepresentation(ticketStore.find(realm, null, filters, null, null));
|
||||||
|
}
|
||||||
|
return userSharedResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceBean getResource() {
|
||||||
|
if (resource == null) {
|
||||||
|
String resourceId = uriInfo.getPathParameters().getFirst("resource_id");
|
||||||
|
|
||||||
|
if (resourceId != null) {
|
||||||
|
resource = getResource(resourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceBean getResource(String id) {
|
||||||
|
return new ResourceBean(
|
||||||
|
authorization.getStoreFactory().getResourceStore().findById(realm, null, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RequesterBean {
|
||||||
|
|
||||||
|
private final Long createdTimestamp;
|
||||||
|
private final Long grantedTimestamp;
|
||||||
|
private UserModel requester;
|
||||||
|
private List<PermissionScopeBean> scopes = new ArrayList<>();
|
||||||
|
private boolean granted;
|
||||||
|
|
||||||
|
public RequesterBean(PermissionTicket ticket, AuthorizationProvider authorization) {
|
||||||
|
this.requester =
|
||||||
|
authorization
|
||||||
|
.getKeycloakSession()
|
||||||
|
.users()
|
||||||
|
.getUserById(authorization.getRealm(), ticket.getRequester());
|
||||||
|
granted = ticket.isGranted();
|
||||||
|
createdTimestamp = ticket.getCreatedTimestamp();
|
||||||
|
grantedTimestamp = ticket.getGrantedTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserModel getRequester() {
|
||||||
|
return requester;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PermissionScopeBean> getScopes() {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addScope(PermissionTicket ticket) {
|
||||||
|
if (ticket != null) {
|
||||||
|
scopes.add(new PermissionScopeBean(ticket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGranted() {
|
||||||
|
return (granted && scopes.isEmpty())
|
||||||
|
|| scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).count()
|
||||||
|
> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreatedDate() {
|
||||||
|
return Time.toDate(createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getGrantedDate() {
|
||||||
|
if (grantedTimestamp == null) {
|
||||||
|
PermissionScopeBean permission =
|
||||||
|
scopes.stream()
|
||||||
|
.filter(permissionScopeBean -> permissionScopeBean.isGranted())
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (permission == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission.getGrantedDate();
|
||||||
|
}
|
||||||
|
return Time.toDate(grantedTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PermissionScopeBean {
|
||||||
|
|
||||||
|
private final Scope scope;
|
||||||
|
private final PermissionTicket ticket;
|
||||||
|
|
||||||
|
public PermissionScopeBean(PermissionTicket ticket) {
|
||||||
|
this.ticket = ticket;
|
||||||
|
scope = ticket.getScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return ticket.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Scope getScope() {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGranted() {
|
||||||
|
return ticket.isGranted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date getGrantedDate() {
|
||||||
|
if (isGranted()) {
|
||||||
|
return Time.toDate(ticket.getGrantedTimestamp());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResourceBean {
|
||||||
|
|
||||||
|
private final ResourceServerBean resourceServer;
|
||||||
|
private final String ownerName;
|
||||||
|
private final UserModel userOwner;
|
||||||
|
private ClientModel clientOwner;
|
||||||
|
private Resource resource;
|
||||||
|
private Map<String, RequesterBean> permissions = new HashMap<>();
|
||||||
|
private Collection<RequesterBean> shares;
|
||||||
|
|
||||||
|
public ResourceBean(Resource resource) {
|
||||||
|
RealmModel realm = authorization.getRealm();
|
||||||
|
ResourceServer resourceServerModel = resource.getResourceServer();
|
||||||
|
resourceServer =
|
||||||
|
new ResourceServerBean(
|
||||||
|
realm.getClientById(resourceServerModel.getClientId()), resourceServerModel);
|
||||||
|
this.resource = resource;
|
||||||
|
userOwner =
|
||||||
|
authorization.getKeycloakSession().users().getUserById(realm, resource.getOwner());
|
||||||
|
if (userOwner == null) {
|
||||||
|
clientOwner = realm.getClientById(resource.getOwner());
|
||||||
|
ownerName = clientOwner.getClientId();
|
||||||
|
} else if (userOwner.getEmail() != null) {
|
||||||
|
ownerName = userOwner.getEmail();
|
||||||
|
} else {
|
||||||
|
ownerName = userOwner.getUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return resource.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return resource.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return resource.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIconUri() {
|
||||||
|
return resource.getIconUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwnerName() {
|
||||||
|
return ownerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserModel getUserOwner() {
|
||||||
|
return userOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientModel getClientOwner() {
|
||||||
|
return clientOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScopeRepresentation> getScopes() {
|
||||||
|
return resource.getScopes().stream()
|
||||||
|
.map(ModelToRepresentation::toRepresentation)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<RequesterBean> getShares() {
|
||||||
|
if (shares == null) {
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters =
|
||||||
|
new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||||
|
|
||||||
|
filters.put(PermissionTicket.FilterOption.RESOURCE_ID, this.resource.getId());
|
||||||
|
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
|
||||||
|
|
||||||
|
shares = toPermissionRepresentation(findPermissions(filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ManagedPermissionBean> getPolicies() {
|
||||||
|
ResourceServer resourceServer = getResourceServer().getResourceServerModel();
|
||||||
|
RealmModel realm = resourceServer.getRealm();
|
||||||
|
Map<Policy.FilterOption, String[]> filters = new EnumMap<>(Policy.FilterOption.class);
|
||||||
|
|
||||||
|
filters.put(Policy.FilterOption.TYPE, new String[] {"uma"});
|
||||||
|
filters.put(Policy.FilterOption.RESOURCE_ID, new String[] {this.resource.getId()});
|
||||||
|
if (getUserOwner() != null) {
|
||||||
|
filters.put(Policy.FilterOption.OWNER, new String[] {getUserOwner().getId()});
|
||||||
|
} else {
|
||||||
|
filters.put(Policy.FilterOption.OWNER, new String[] {getClientOwner().getId()});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Policy> policies =
|
||||||
|
authorization
|
||||||
|
.getStoreFactory()
|
||||||
|
.getPolicyStore()
|
||||||
|
.find(realm, resourceServer, filters, null, null);
|
||||||
|
|
||||||
|
if (policies.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return policies.stream()
|
||||||
|
.filter(
|
||||||
|
policy -> {
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters1 =
|
||||||
|
new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||||
|
|
||||||
|
filters1.put(PermissionTicket.FilterOption.POLICY_ID, policy.getId());
|
||||||
|
|
||||||
|
return authorization
|
||||||
|
.getStoreFactory()
|
||||||
|
.getPermissionTicketStore()
|
||||||
|
.find(realm, resourceServer, filters1, -1, 1)
|
||||||
|
.isEmpty();
|
||||||
|
})
|
||||||
|
.map(ManagedPermissionBean::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceServerBean getResourceServer() {
|
||||||
|
return resourceServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<RequesterBean> getPermissions() {
|
||||||
|
return permissions.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addPermission(PermissionTicket ticket, AuthorizationProvider authorization) {
|
||||||
|
permissions
|
||||||
|
.computeIfAbsent(ticket.getRequester(), key -> new RequesterBean(ticket, authorization))
|
||||||
|
.addScope(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<RequesterBean> toPermissionRepresentation(
|
||||||
|
List<PermissionTicket> permissionRequests) {
|
||||||
|
Map<String, RequesterBean> requests = new HashMap<>();
|
||||||
|
|
||||||
|
for (PermissionTicket ticket : permissionRequests) {
|
||||||
|
Resource resource = ticket.getResource();
|
||||||
|
|
||||||
|
if (!resource.isOwnerManagedAccess()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
requests
|
||||||
|
.computeIfAbsent(
|
||||||
|
ticket.getRequester(), resourceId -> new RequesterBean(ticket, authorization))
|
||||||
|
.addScope(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<ResourceBean> toResourceRepresentation(List<PermissionTicket> tickets) {
|
||||||
|
Map<String, ResourceBean> requests = new HashMap<>();
|
||||||
|
|
||||||
|
for (PermissionTicket ticket : tickets) {
|
||||||
|
Resource resource = ticket.getResource();
|
||||||
|
|
||||||
|
if (!resource.isOwnerManagedAccess()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
requests
|
||||||
|
.computeIfAbsent(resource.getId(), resourceId -> getResource(resourceId))
|
||||||
|
.addPermission(ticket, authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PermissionTicket> findPermissions(
|
||||||
|
Map<PermissionTicket.FilterOption, String> filters) {
|
||||||
|
return authorization
|
||||||
|
.getStoreFactory()
|
||||||
|
.getPermissionTicketStore()
|
||||||
|
.find(realm, null, filters, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResourceServerBean {
|
||||||
|
|
||||||
|
private ClientModel clientModel;
|
||||||
|
private ResourceServer resourceServer;
|
||||||
|
|
||||||
|
public ResourceServerBean(ClientModel clientModel, ResourceServer resourceServer) {
|
||||||
|
this.clientModel = clientModel;
|
||||||
|
this.resourceServer = resourceServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return resourceServer.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
String name = clientModel.getName();
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientModel.getClientId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return clientModel.getClientId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUri() {
|
||||||
|
Set<String> redirectUris = clientModel.getRedirectUris();
|
||||||
|
|
||||||
|
if (redirectUris.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirectUris.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBaseUri() {
|
||||||
|
return ResolveRelative.resolveRelativeUri(
|
||||||
|
session, clientModel.getRootUrl(), clientModel.getBaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceServer getResourceServerModel() {
|
||||||
|
return resourceServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ManagedPermissionBean {
|
||||||
|
|
||||||
|
private final Policy policy;
|
||||||
|
private List<ManagedPermissionBean> policies;
|
||||||
|
|
||||||
|
public ManagedPermissionBean(Policy policy) {
|
||||||
|
this.policy = policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return policy.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ScopeRepresentation> getScopes() {
|
||||||
|
return policy.getScopes().stream()
|
||||||
|
.map(ModelToRepresentation::toRepresentation)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return this.policy.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ManagedPermissionBean> getPolicies() {
|
||||||
|
if (this.policies == null) {
|
||||||
|
this.policies =
|
||||||
|
policy.getAssociatedPolicies().stream()
|
||||||
|
.map(ManagedPermissionBean::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.policies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class FeaturesBean {
|
||||||
|
|
||||||
|
private final boolean identityFederation;
|
||||||
|
private final boolean log;
|
||||||
|
private final boolean passwordUpdateSupported;
|
||||||
|
private boolean authorization;
|
||||||
|
|
||||||
|
public FeaturesBean(
|
||||||
|
boolean identityFederation,
|
||||||
|
boolean log,
|
||||||
|
boolean passwordUpdateSupported,
|
||||||
|
boolean authorization) {
|
||||||
|
this.identityFederation = identityFederation;
|
||||||
|
this.log = log;
|
||||||
|
this.passwordUpdateSupported = passwordUpdateSupported;
|
||||||
|
this.authorization = authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIdentityFederation() {
|
||||||
|
return identityFederation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLog() {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordUpdateSupported() {
|
||||||
|
return passwordUpdateSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthorization() {
|
||||||
|
return authorization;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.keycloak.events.Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class LogBean {
|
||||||
|
|
||||||
|
private List<EventBean> events;
|
||||||
|
|
||||||
|
public LogBean(List<Event> events) {
|
||||||
|
this.events = new LinkedList<EventBean>();
|
||||||
|
for (Event e : events) {
|
||||||
|
this.events.add(new EventBean(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EventBean> getEvents() {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EventBean {
|
||||||
|
|
||||||
|
private Event event;
|
||||||
|
|
||||||
|
public EventBean(Event event) {
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getDate() {
|
||||||
|
return new Date(event.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEvent() {
|
||||||
|
return event.getType().toString().toLowerCase().replace("_", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClient() {
|
||||||
|
return event.getClientId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIpAddress() {
|
||||||
|
return event.getIpAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DetailBean> getDetails() {
|
||||||
|
List<DetailBean> details = new LinkedList<DetailBean>();
|
||||||
|
if (event.getDetails() != null) {
|
||||||
|
for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
|
||||||
|
details.add(new DetailBean(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DetailBean {
|
||||||
|
|
||||||
|
private Map.Entry<String, String> entry;
|
||||||
|
|
||||||
|
public DetailBean(Map.Entry<String, String> entry) {
|
||||||
|
this.entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return entry.getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return entry.getValue().replace("_", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class PasswordBean {
|
||||||
|
|
||||||
|
private boolean passwordSet;
|
||||||
|
|
||||||
|
public PasswordBean(boolean passwordSet) {
|
||||||
|
this.passwordSet = passwordSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordSet() {
|
||||||
|
return passwordSet;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
||||||
|
*/
|
||||||
|
public class RealmBean {
|
||||||
|
|
||||||
|
private RealmModel realm;
|
||||||
|
|
||||||
|
public RealmBean(RealmModel realmModel) {
|
||||||
|
realm = realmModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return realm.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
String displayName = realm.getDisplayName();
|
||||||
|
if (displayName != null && displayName.length() > 0) {
|
||||||
|
return displayName;
|
||||||
|
} else {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayNameHtml() {
|
||||||
|
String displayNameHtml = realm.getDisplayNameHtml();
|
||||||
|
if (displayNameHtml != null && displayNameHtml.length() > 0) {
|
||||||
|
return displayNameHtml;
|
||||||
|
} else {
|
||||||
|
return getDisplayName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInternationalizationEnabled() {
|
||||||
|
return realm.isInternationalizationEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getSupportedLocales() {
|
||||||
|
return realm.getSupportedLocalesStream().collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEditUsernameAllowed() {
|
||||||
|
return realm.isEditUsernameAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRegistrationEmailAsUsername() {
|
||||||
|
return realm.isRegistrationEmailAsUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUserManagedAccessAllowed() {
|
||||||
|
return realm.isUserManagedAccessAllowed();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ReferrerBean {
|
||||||
|
|
||||||
|
private String[] referrer;
|
||||||
|
|
||||||
|
public ReferrerBean(String[] referrer) {
|
||||||
|
this.referrer = referrer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return referrer[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return referrer[1];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class SessionsBean {
|
||||||
|
|
||||||
|
private List<UserSessionBean> events;
|
||||||
|
private RealmModel realm;
|
||||||
|
|
||||||
|
public SessionsBean(RealmModel realm, List<UserSessionModel> sessions) {
|
||||||
|
this.events = new LinkedList<>();
|
||||||
|
for (UserSessionModel session : sessions) {
|
||||||
|
this.events.add(new UserSessionBean(realm, session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSessionBean> getSessions() {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserSessionBean {
|
||||||
|
|
||||||
|
private UserSessionModel session;
|
||||||
|
private RealmModel realm;
|
||||||
|
|
||||||
|
public UserSessionBean(RealmModel realm, UserSessionModel session) {
|
||||||
|
this.realm = realm;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return session.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIpAddress() {
|
||||||
|
return session.getIpAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getStarted() {
|
||||||
|
return Time.toDate(session.getStarted());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLastAccess() {
|
||||||
|
return Time.toDate(session.getLastSessionRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getExpires() {
|
||||||
|
int maxLifespan =
|
||||||
|
session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0
|
||||||
|
? realm.getSsoSessionMaxLifespanRememberMe()
|
||||||
|
: realm.getSsoSessionMaxLifespan();
|
||||||
|
int max = session.getStarted() + maxLifespan;
|
||||||
|
return Time.toDate(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getClients() {
|
||||||
|
Set<String> clients = new HashSet<>();
|
||||||
|
for (String clientUUID : session.getAuthenticatedClientSessions().keySet()) {
|
||||||
|
ClientModel client = realm.getClientById(clientUUID);
|
||||||
|
clients.add(client.getClientId());
|
||||||
|
}
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRepresentation;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.keycloak.authentication.otp.OTPApplicationProvider;
|
||||||
|
import org.keycloak.credential.CredentialModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.OTPCredentialModel;
|
||||||
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.utils.TotpUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class TotpBean {
|
||||||
|
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final String totpSecret;
|
||||||
|
private final String totpSecretEncoded;
|
||||||
|
private final String totpSecretQrCode;
|
||||||
|
private final boolean enabled;
|
||||||
|
private KeycloakSession session;
|
||||||
|
private final UriBuilder uriBuilder;
|
||||||
|
private final List<CredentialModel> otpCredentials;
|
||||||
|
private final List<String> supportedApplications;
|
||||||
|
|
||||||
|
public TotpBean(
|
||||||
|
KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
|
||||||
|
this.session = session;
|
||||||
|
this.uriBuilder = uriBuilder;
|
||||||
|
this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE);
|
||||||
|
if (enabled) {
|
||||||
|
List<CredentialModel> otpCredentials =
|
||||||
|
user.credentialManager()
|
||||||
|
.getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (otpCredentials.isEmpty()) {
|
||||||
|
// Credential is configured on userStorage side. Create the "fake" credential similar like
|
||||||
|
// we do for the new account console
|
||||||
|
CredentialRepresentation credential =
|
||||||
|
createUserStorageCredentialRepresentation(OTPCredentialModel.TYPE);
|
||||||
|
this.otpCredentials = Collections.singletonList(RepresentationToModel.toModel(credential));
|
||||||
|
} else {
|
||||||
|
this.otpCredentials = otpCredentials;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.otpCredentials = Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.realm = realm;
|
||||||
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
|
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||||
|
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||||
|
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
this.supportedApplications =
|
||||||
|
session.getAllProviders(OTPApplicationProvider.class).stream()
|
||||||
|
.filter(p -> p.supports(otpPolicy))
|
||||||
|
.map(OTPApplicationProvider::getName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpSecret() {
|
||||||
|
return totpSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpSecretEncoded() {
|
||||||
|
return totpSecretEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpSecretQrCode() {
|
||||||
|
return totpSecretQrCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getManualUrl() {
|
||||||
|
return uriBuilder.replaceQueryParam("mode", "manual").build().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQrUrl() {
|
||||||
|
return uriBuilder.replaceQueryParam("mode", "qr").build().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OTPPolicy getPolicy() {
|
||||||
|
return realm.getOTPPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getSupportedApplications() {
|
||||||
|
return supportedApplications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CredentialModel> getOtpCredentials() {
|
||||||
|
return otpCredentials;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.AccountUrls;
|
||||||
|
import org.keycloak.theme.Theme;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class UrlBean {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(UrlBean.class);
|
||||||
|
private String realm;
|
||||||
|
private Theme theme;
|
||||||
|
private URI baseURI;
|
||||||
|
private URI baseQueryURI;
|
||||||
|
private URI currentURI;
|
||||||
|
private String idTokenHint;
|
||||||
|
|
||||||
|
public UrlBean(
|
||||||
|
RealmModel realm,
|
||||||
|
Theme theme,
|
||||||
|
URI baseURI,
|
||||||
|
URI baseQueryURI,
|
||||||
|
URI currentURI,
|
||||||
|
String idTokenHint) {
|
||||||
|
this.realm = realm.getName();
|
||||||
|
this.theme = theme;
|
||||||
|
this.baseURI = baseURI;
|
||||||
|
this.baseQueryURI = baseQueryURI;
|
||||||
|
this.currentURI = currentURI;
|
||||||
|
this.idTokenHint = idTokenHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getApplicationsUrl() {
|
||||||
|
return AccountUrls.accountApplicationsPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccountUrl() {
|
||||||
|
return AccountUrls.accountPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPasswordUrl() {
|
||||||
|
return AccountUrls.accountPasswordPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSocialUrl() {
|
||||||
|
return AccountUrls.accountFederatedIdentityPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpUrl() {
|
||||||
|
return AccountUrls.accountTotpPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogUrl() {
|
||||||
|
return AccountUrls.accountLogPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionsUrl() {
|
||||||
|
return AccountUrls.accountSessionsPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogoutUrl() {
|
||||||
|
return AccountUrls.accountLogout(baseQueryURI, currentURI, realm, idTokenHint).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceUrl() {
|
||||||
|
return AccountUrls.accountResourcesPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceDetailUrl(String id) {
|
||||||
|
return AccountUrls.accountResourceDetailPage(id, baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceGrant(String id) {
|
||||||
|
return AccountUrls.accountResourceGrant(id, baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceShare(String id) {
|
||||||
|
return AccountUrls.accountResourceShare(id, baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourcesPath() {
|
||||||
|
URI uri = AccountUrls.themeRoot(baseURI);
|
||||||
|
return uri.getPath() + "/" + theme.getType().toString().toLowerCase() + "/" + theme.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourcesCommonPath() {
|
||||||
|
URI uri = AccountUrls.themeRoot(baseURI);
|
||||||
|
String commonPath = "";
|
||||||
|
try {
|
||||||
|
commonPath = theme.getProperties().getProperty("import");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
logger.warn("Failed to load properties", ex);
|
||||||
|
}
|
||||||
|
if (commonPath == null || commonPath.isEmpty()) {
|
||||||
|
commonPath = "/common/keycloak";
|
||||||
|
}
|
||||||
|
return uri.getPath() + "/" + commonPath;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
package org.keycloak.services;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URI;
|
||||||
|
import lombok.extern.jbosslog.JBossLog;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
|
import org.keycloak.services.resources.account.AccountFormService;
|
||||||
|
|
||||||
|
@JBossLog
|
||||||
|
public class AccountUrls extends Urls {
|
||||||
|
|
||||||
|
private static UriBuilder realmLogout(URI baseUri) {
|
||||||
|
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UriBuilder accountBase(URI baseUri) {
|
||||||
|
return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UriBuilder tokenBase(URI baseUri) {
|
||||||
|
return realmBase(baseUri).path("{realm}/protocol/" + OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountApplicationsPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "applicationsPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountPage(URI baseUri, String realmName) {
|
||||||
|
return accountPageBuilder(baseUri).build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UriBuilder accountPageBuilder(URI baseUri) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "accountPage");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountPasswordPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "passwordPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountFederatedIdentityPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri)
|
||||||
|
.path(AccountFormService.class, "federatedIdentityPage")
|
||||||
|
.build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountFederatedIdentityUpdate(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri)
|
||||||
|
.path(AccountFormService.class, "processFederatedIdentityUpdate")
|
||||||
|
.build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountTotpPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountLogPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "logPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountSessionsPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountLogout(
|
||||||
|
URI baseUri, URI redirectUri, String realmName, String idTokenHint) {
|
||||||
|
return realmLogout(baseUri)
|
||||||
|
.queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri)
|
||||||
|
.queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint)
|
||||||
|
.build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountResourcesPage(URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri).path(AccountFormService.class, "resourcesPage").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountResourceDetailPage(String resourceId, URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri)
|
||||||
|
.path(AccountFormService.class, "resourceDetailPage")
|
||||||
|
.build(realmName, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountResourceGrant(String resourceId, URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri)
|
||||||
|
.path(AccountFormService.class, "grantPermission")
|
||||||
|
.build(realmName, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountResourceShare(String resourceId, URI baseUri, String realmName) {
|
||||||
|
return accountBase(baseUri)
|
||||||
|
.path(AccountFormService.class, "shareResource")
|
||||||
|
.build(realmName, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI loginActionUpdatePassword(URI baseUri, String realmName) {
|
||||||
|
return loginActionsBase(baseUri)
|
||||||
|
.path(LoginActionsService.class, "updatePassword")
|
||||||
|
.build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI loginActionUpdateTotp(URI baseUri, String realmName) {
|
||||||
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI loginActionEmailVerification(URI baseUri, String realmName) {
|
||||||
|
return loginActionEmailVerificationBuilder(baseUri).build(realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String localeCookiePath(URI baseUri, String realmName) {
|
||||||
|
return realmBase(baseUri).path(realmName).build().getRawPath();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,64 @@
|
|||||||
|
package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
|
import com.google.auto.service.AutoService;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.jbosslog.JBossLog;
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.ProviderEvent;
|
||||||
|
import org.keycloak.services.resource.AccountResourceProvider;
|
||||||
|
import org.keycloak.services.resource.AccountResourceProviderFactory;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
|
||||||
|
@JBossLog
|
||||||
|
@AutoService(AccountResourceProviderFactory.class)
|
||||||
|
public class AccountFormServiceFactory implements AccountResourceProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = "account-v1";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientModel getAccountManagementClient(RealmModel realm) {
|
||||||
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
|
if (client == null || !client.isEnabled()) {
|
||||||
|
log.debug("account management not enabled");
|
||||||
|
throw new NotFoundException("account management not enabled");
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountResourceProvider create(KeycloakSession session) {
|
||||||
|
log.info("create");
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
ClientModel client = getAccountManagementClient(realm);
|
||||||
|
EventBuilder event = new EventBuilder(realm, session, session.getContext().getConnection());
|
||||||
|
return new AccountFormService(session, client, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope config) {
|
||||||
|
log.info("init");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
log.info("postInit");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
log.info("close");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,249 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import type { ThemeType } from "../generateFtl";
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
||||||
|
import { transformCodebase } from "../../tools/transformCodebase";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
extraThemeNames: string[];
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
themeVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accountV1Keycloak = "account-v1-keycloak";
|
||||||
|
|
||||||
|
export async function generateJavaStackFiles(params: {
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
implementedThemeTypes: Record<ThemeType | "email", boolean>;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}): Promise<{
|
||||||
|
jarFilePath: string;
|
||||||
|
}> {
|
||||||
|
const {
|
||||||
|
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
implementedThemeTypes
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
{
|
||||||
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
|
pomFileCode: string;
|
||||||
|
} {
|
||||||
|
const pomFileCode = [
|
||||||
|
`<?xml version="1.0"?>`,
|
||||||
|
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
||||||
|
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
||||||
|
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
||||||
|
` <modelVersion>4.0.0</modelVersion>`,
|
||||||
|
` <groupId>${groupId}</groupId>`,
|
||||||
|
` <artifactId>${artifactId}</artifactId>`,
|
||||||
|
` <version>${themeVersion}</version>`,
|
||||||
|
` <name>${artifactId}</name>`,
|
||||||
|
` <description />`,
|
||||||
|
` <packaging>jar</packaging>`,
|
||||||
|
` <properties>`,
|
||||||
|
` <java.version>17</java.version>`,
|
||||||
|
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
|
||||||
|
` <keycloak.version>999.0.0-SNAPSHOT</keycloak.version>`,
|
||||||
|
` <guava.version>32.0.0-jre</guava.version>`,
|
||||||
|
` <lombok.version>1.18.28</lombok.version>`,
|
||||||
|
` <auto-service.version>1.1.1</auto-service.version>`,
|
||||||
|
` </properties>`,
|
||||||
|
` <build>`,
|
||||||
|
` <plugins>`,
|
||||||
|
` <plugin>`,
|
||||||
|
` <artifactId>maven-compiler-plugin</artifactId>`,
|
||||||
|
` <version>3.11.0</version>`,
|
||||||
|
` <configuration>`,
|
||||||
|
` <source>\${java.version}</source>`,
|
||||||
|
` <target>\${java.version}</target>`,
|
||||||
|
` <compilerArgument>-Xlint:unchecked</compilerArgument>`,
|
||||||
|
` <compilerArgument>-Xlint:deprecation</compilerArgument>`,
|
||||||
|
` <useIncrementalCompilation>false</useIncrementalCompilation>`,
|
||||||
|
` <annotationProcessorPaths>`,
|
||||||
|
` <path>`,
|
||||||
|
` <groupId>com.google.auto.service</groupId>`,
|
||||||
|
` <artifactId>auto-service</artifactId>`,
|
||||||
|
` <version>\${auto-service.version}</version>`,
|
||||||
|
` </path>`,
|
||||||
|
` <path>`,
|
||||||
|
` <groupId>org.projectlombok</groupId>`,
|
||||||
|
` <artifactId>lombok</artifactId>`,
|
||||||
|
` <version>\${lombok.version}</version>`,
|
||||||
|
` </path>`,
|
||||||
|
` </annotationProcessorPaths>`,
|
||||||
|
` </configuration>`,
|
||||||
|
` </plugin>`,
|
||||||
|
` <plugin>`,
|
||||||
|
` <groupId>org.apache.maven.plugins</groupId>`,
|
||||||
|
` <artifactId>maven-jar-plugin</artifactId>`,
|
||||||
|
` <version>3.2.0</version>`,
|
||||||
|
` <configuration>`,
|
||||||
|
` <archive>`,
|
||||||
|
` <manifestEntries>`,
|
||||||
|
` <Dependencies>`,
|
||||||
|
` <![CDATA[org.keycloak.keycloak-common,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,com.google.guava]]>`,
|
||||||
|
` </Dependencies>`,
|
||||||
|
` </manifestEntries>`,
|
||||||
|
` </archive>`,
|
||||||
|
` </configuration>`,
|
||||||
|
` </plugin>`,
|
||||||
|
` <plugin>`,
|
||||||
|
` <groupId>com.spotify.fmt</groupId>`,
|
||||||
|
` <artifactId>fmt-maven-plugin</artifactId>`,
|
||||||
|
` <version>2.20</version>`,
|
||||||
|
` </plugin>`,
|
||||||
|
` </plugins>`,
|
||||||
|
` </build>`,
|
||||||
|
` <dependencies>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>org.projectlombok</groupId>`,
|
||||||
|
` <artifactId>lombok</artifactId>`,
|
||||||
|
` <version>\${lombok.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>com.google.auto.service</groupId>`,
|
||||||
|
` <artifactId>auto-service</artifactId>`,
|
||||||
|
` <version>\${auto-service.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>org.keycloak</groupId>`,
|
||||||
|
` <artifactId>keycloak-server-spi</artifactId>`,
|
||||||
|
` <version>\${keycloak.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>org.keycloak</groupId>`,
|
||||||
|
` <artifactId>keycloak-server-spi-private</artifactId>`,
|
||||||
|
` <version>\${keycloak.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>org.keycloak</groupId>`,
|
||||||
|
` <artifactId>keycloak-services</artifactId>`,
|
||||||
|
` <version>\${keycloak.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>jakarta.ws.rs</groupId>`,
|
||||||
|
` <artifactId>jakarta.ws.rs-api</artifactId>`,
|
||||||
|
` <version>3.1.0</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` <dependency>`,
|
||||||
|
` <groupId>com.google.guava</groupId>`,
|
||||||
|
` <artifactId>guava</artifactId>`,
|
||||||
|
` <version>\${guava.version}</version>`,
|
||||||
|
` <scope>provided</scope>`,
|
||||||
|
` </dependency>`,
|
||||||
|
` </dependencies>`,
|
||||||
|
`</project>`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
return { pomFileCode };
|
||||||
|
})();
|
||||||
|
|
||||||
|
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountV1 = "account-v1";
|
||||||
|
|
||||||
|
{
|
||||||
|
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||||
|
"isSilent": true,
|
||||||
|
"keycloakVersion": "21.1.2"
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
|
||||||
|
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", accountV1, "account")
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common"),
|
||||||
|
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", accountV1Keycloak, "common")
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account"),
|
||||||
|
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", accountV1Keycloak, "account"),
|
||||||
|
"transformSourceCode": ({ sourceCode, filePath }) => {
|
||||||
|
if (pathBasename(filePath) === "theme.properties") {
|
||||||
|
sourceCode = Buffer.from(sourceCode.toString("utf8").replace("parent=base", `parent=${accountV1}`), "utf8");
|
||||||
|
sourceCode = Buffer.from(
|
||||||
|
sourceCode.toString("utf8").replace("import=common/keycloak", `import=common/${accountV1Keycloak}`),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"modifiedSourceCode": sourceCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.rmdirSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(__dirname, "account-v1-java"),
|
||||||
|
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "java", "org", "keycloak")
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(pathDirname(themeManifestFilePath));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
themeManifestFilePath,
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "account-v1",
|
||||||
|
"types": ["account"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "account-v1-keycloak",
|
||||||
|
"types": ["account"]
|
||||||
|
},
|
||||||
|
...[themeName, ...extraThemeNames].map(themeName => ({
|
||||||
|
"name": themeName,
|
||||||
|
"types": Object.entries(implementedThemeTypes)
|
||||||
|
.filter(([, isImplemented]) => isImplemented)
|
||||||
|
.map(([themeType]) => themeType)
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
|
||||||
|
};
|
||||||
|
}
|
1
src/bin/keycloakify/generateJavaStackFiles/index.ts
Normal file
1
src/bin/keycloakify/generateJavaStackFiles/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./generateJavaStackFiles";
|
@ -1,6 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
import type { BuildOptions } from "./BuildOptions";
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = {
|
||||||
@ -8,7 +9,11 @@ export type BuildOptionsLike = {
|
|||||||
extraThemeNames: string[];
|
extraThemeNames: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
|
@ -13,17 +13,13 @@ import * as crypto from "crypto";
|
|||||||
export async function downloadKeycloakStaticResources(
|
export async function downloadKeycloakStaticResources(
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
params: {
|
params: {
|
||||||
projectDirPath: string;
|
|
||||||
themeType: ThemeType;
|
themeType: ThemeType;
|
||||||
themeDirPath: string;
|
themeDirPath: string;
|
||||||
|
isSilent: boolean;
|
||||||
keycloakVersion: string;
|
keycloakVersion: string;
|
||||||
usedResources: {
|
|
||||||
resourcesCommonFilePaths: string[];
|
|
||||||
resourcesFilePaths: string[];
|
|
||||||
} | undefined
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { projectDirPath, themeType, themeDirPath, keycloakVersion, usedResources } = params;
|
const { themeType, isSilent, themeDirPath, keycloakVersion } = params;
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(
|
const tmpDirPath = pathJoin(
|
||||||
themeDirPath,
|
themeDirPath,
|
||||||
@ -32,39 +28,19 @@ export async function downloadKeycloakStaticResources(
|
|||||||
);
|
);
|
||||||
|
|
||||||
await downloadBuiltinKeycloakTheme({
|
await downloadBuiltinKeycloakTheme({
|
||||||
projectDirPath,
|
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": tmpDirPath
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir)),
|
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir))
|
||||||
"transformSourceCode":
|
|
||||||
usedResources === undefined
|
|
||||||
? undefined
|
|
||||||
: ({ fileRelativePath, sourceCode }) => {
|
|
||||||
if (!usedResources.resourcesFilePaths.includes(fileRelativePath)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { "modifiedSourceCode": sourceCode };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)),
|
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir))
|
||||||
"transformSourceCode":
|
|
||||||
usedResources === undefined
|
|
||||||
? undefined
|
|
||||||
: ({ fileRelativePath, sourceCode }) => {
|
|
||||||
if (!usedResources.resourcesCommonFilePaths.includes(fileRelativePath)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { "modifiedSourceCode": sourceCode };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||||
|
@ -12,20 +12,46 @@ import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResourc
|
|||||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||||
import { generateMessageProperties } from "./generateMessageProperties";
|
import { generateMessageProperties } from "./generateMessageProperties";
|
||||||
import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
|
import { accountV1Keycloak } from "../generateJavaStackFiles/generateJavaStackFiles";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
themeName: string;
|
|
||||||
extraThemeProperties: string[] | undefined;
|
export namespace BuildOptionsLike {
|
||||||
themeVersion: string;
|
export type Common = {
|
||||||
keycloakVersionDefaultAssets: string;
|
themeName: string;
|
||||||
urlPathname: string | undefined;
|
extraThemeProperties: string[] | undefined;
|
||||||
};
|
isSilent: boolean;
|
||||||
|
themeVersion: string;
|
||||||
|
keycloakVersionDefaultAssets: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
export async function generateTheme(params: {
|
export async function generateTheme(params: {
|
||||||
projectDirPath: string;
|
|
||||||
reactAppBuildDirPath: string;
|
reactAppBuildDirPath: string;
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
@ -33,15 +59,7 @@ export async function generateTheme(params: {
|
|||||||
buildOptions: BuildOptionsLike;
|
buildOptions: BuildOptionsLike;
|
||||||
keycloakifyVersion: string;
|
keycloakifyVersion: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
|
||||||
projectDirPath,
|
|
||||||
reactAppBuildDirPath,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
themeSrcDirPath,
|
|
||||||
keycloakifySrcDirPath,
|
|
||||||
buildOptions,
|
|
||||||
keycloakifyVersion
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
||||||
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
||||||
@ -60,16 +78,17 @@ export async function generateTheme(params: {
|
|||||||
copy_app_resources_to_theme_path: {
|
copy_app_resources_to_theme_path: {
|
||||||
const isFirstPass = themeType.indexOf(themeType) === 0;
|
const isFirstPass = themeType.indexOf(themeType) === 0;
|
||||||
|
|
||||||
if (!isFirstPass) {
|
if (!isFirstPass && !buildOptions.isStandalone) {
|
||||||
break copy_app_resources_to_theme_path;
|
break copy_app_resources_to_theme_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"destDirPath": pathJoin(themeDirPath, "resources", "build"),
|
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||||
"srcDirPath": reactAppBuildDirPath,
|
"srcDirPath": reactAppBuildDirPath,
|
||||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||||
if (
|
if (
|
||||||
|
buildOptions.isStandalone &&
|
||||||
isInside({
|
isInside({
|
||||||
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
|
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
|
||||||
filePath
|
filePath
|
||||||
@ -79,6 +98,10 @@ export async function generateTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/\.css?$/i.test(filePath)) {
|
if (/\.css?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||||
"cssCode": sourceCode.toString("utf8")
|
"cssCode": sourceCode.toString("utf8")
|
||||||
});
|
});
|
||||||
@ -98,14 +121,19 @@ export async function generateTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/\.js?$/i.test(filePath)) {
|
if (/\.js?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": sourceCode.toString("utf8")
|
"jsCode": sourceCode.toString("utf8"),
|
||||||
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { "modifiedSourceCode": sourceCode };
|
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -170,11 +198,10 @@ export async function generateTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await downloadKeycloakStaticResources({
|
await downloadKeycloakStaticResources({
|
||||||
projectDirPath,
|
"isSilent": buildOptions.isSilent,
|
||||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||||
"themeDirPath": keycloakDirInPublicDir,
|
"themeDirPath": keycloakDirInPublicDir,
|
||||||
themeType,
|
themeType
|
||||||
"usedResources": undefined
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (themeType !== themeTypes[0]) {
|
if (themeType !== themeTypes[0]) {
|
||||||
@ -196,20 +223,28 @@ export async function generateTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await downloadKeycloakStaticResources({
|
await downloadKeycloakStaticResources({
|
||||||
projectDirPath,
|
"isSilent": buildOptions.isSilent,
|
||||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||||
themeDirPath,
|
themeDirPath,
|
||||||
themeType,
|
themeType
|
||||||
"usedResources": readStaticResourcesUsage({
|
|
||||||
keycloakifySrcDirPath,
|
|
||||||
themeSrcDirPath,
|
|
||||||
themeType
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(themeDirPath, "theme.properties"),
|
pathJoin(themeDirPath, "theme.properties"),
|
||||||
Buffer.from([`parent=keycloak`, ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
|
Buffer.from(
|
||||||
|
[
|
||||||
|
`parent=${(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return "keycloak";
|
||||||
|
case "account":
|
||||||
|
return accountV1Keycloak;
|
||||||
|
}
|
||||||
|
})()}`,
|
||||||
|
...(buildOptions.extraThemeProperties ?? [])
|
||||||
|
].join("\n\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import type { ThemeType } from "../generateFtl";
|
import type { ThemeType } from "../generateFtl";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
/** Assumes the theme type exists */
|
/** Assumes the theme type exists */
|
||||||
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||||
@ -10,7 +11,9 @@ export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; them
|
|||||||
|
|
||||||
const fieldNames: string[] = [];
|
const fieldNames: string[] = [];
|
||||||
|
|
||||||
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
for (const srcDirPath of ([pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)] as const).filter(
|
||||||
|
exclude(undefined)
|
||||||
|
)) {
|
||||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import { crawl } from "../../tools/crawl";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import type { ThemeType } from "../generateFtl";
|
|
||||||
|
|
||||||
/** Assumes the theme type exists */
|
|
||||||
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
|
|
||||||
resourcesCommonFilePaths: string[];
|
|
||||||
resourcesFilePaths: string[];
|
|
||||||
} {
|
|
||||||
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
|
|
||||||
|
|
||||||
const resourcesCommonFilePaths = new Set<string>();
|
|
||||||
const resourcesFilePaths = new Set<string>();
|
|
||||||
|
|
||||||
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
|
||||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
|
|
||||||
|
|
||||||
if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrap = readPaths({ rawSourceFile });
|
|
||||||
|
|
||||||
wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
|
|
||||||
wrap.resourcesFilePaths.forEach(filePath => resourcesFilePaths.add(filePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths),
|
|
||||||
"resourcesFilePaths": Array.from(resourcesFilePaths)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Exported for testing purpose */
|
|
||||||
export function readPaths(params: { rawSourceFile: string }): {
|
|
||||||
resourcesCommonFilePaths: string[];
|
|
||||||
resourcesFilePaths: string[];
|
|
||||||
} {
|
|
||||||
const { rawSourceFile } = params;
|
|
||||||
|
|
||||||
const resourcesCommonFilePaths = new Set<string>();
|
|
||||||
const resourcesFilePaths = new Set<string>();
|
|
||||||
|
|
||||||
for (const isCommon of [true, false]) {
|
|
||||||
const set = isCommon ? resourcesCommonFilePaths : resourcesFilePaths;
|
|
||||||
|
|
||||||
{
|
|
||||||
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*}([^\`]+)\``, "g");
|
|
||||||
|
|
||||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const filePath = match[1];
|
|
||||||
|
|
||||||
set.add(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
|
|
||||||
|
|
||||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const filePath = match[1];
|
|
||||||
|
|
||||||
set.add(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePrefixSlash = (filePath: string) => (filePath.startsWith("/") ? filePath.slice(1) : filePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(removePrefixSlash),
|
|
||||||
"resourcesFilePaths": Array.from(resourcesFilePaths).map(removePrefixSlash)
|
|
||||||
};
|
|
||||||
}
|
|
@ -30,7 +30,6 @@ export async function main() {
|
|||||||
|
|
||||||
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
|
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
|
||||||
await generateTheme({
|
await generateTheme({
|
||||||
projectDirPath,
|
|
||||||
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
|
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||||
themeSrcDirPath,
|
themeSrcDirPath,
|
||||||
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
|
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
|
||||||
@ -49,7 +48,7 @@ export async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
const { jarFilePath } = await generateJavaStackFiles({
|
||||||
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
|
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||||
"implementedThemeTypes": (() => {
|
"implementedThemeTypes": (() => {
|
||||||
const implementedThemeTypes = {
|
const implementedThemeTypes = {
|
||||||
|
@ -1,6 +1,31 @@
|
|||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): { fixedJsCode: string } {
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
|
||||||
/*
|
/*
|
||||||
NOTE:
|
NOTE:
|
||||||
|
|
||||||
@ -13,38 +38,48 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): {
|
|||||||
will always run in keycloak context.
|
will always run in keycloak context.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { jsCode } = params;
|
const { jsCode, buildOptions } = params;
|
||||||
|
|
||||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
|
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||||
(...[, n, u, matchedFunction, eForFunction]) => {
|
(...[, n, u, e]) => `
|
||||||
const isArrowFunction = matchedFunction.includes("=>");
|
${n}[(function(){
|
||||||
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
|
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||||
|
|
||||||
return `
|
|
||||||
${n}[(function(){
|
|
||||||
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
|
|
||||||
if( pd === undefined || pd.configurable ){
|
if( pd === undefined || pd.configurable ){
|
||||||
|
${
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||||
|
set: function (){}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
var p= "";
|
||||||
Object.defineProperty(${n}, "p", {
|
Object.defineProperty(${n}, "p", {
|
||||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
|
||||||
set: function() {}
|
set: function (value){ p = value;}
|
||||||
});
|
});
|
||||||
|
`
|
||||||
}
|
}
|
||||||
return "${u}";
|
}
|
||||||
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
|
return "${u}";
|
||||||
.replace(/\s+/g, " ")
|
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const fixedJsCode = jsCode
|
const fixedJsCode = jsCode
|
||||||
.replace(...getReplaceArgs("js"))
|
.replace(...getReplaceArgs("js"))
|
||||||
.replace(...getReplaceArgs("css"))
|
.replace(...getReplaceArgs("css"))
|
||||||
.replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
|
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||||
|
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
|
||||||
|
)
|
||||||
//TODO: Write a test case for this
|
//TODO: Write a test case for this
|
||||||
.replace(
|
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
||||||
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
|
buildOptions.isStandalone
|
||||||
(...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
|
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||||
|
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
|
||||||
);
|
);
|
||||||
|
|
||||||
return { fixedJsCode };
|
return { fixedJsCode };
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import type { BuildOptions } from "../BuildOptions";
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = {
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||||
fixedCssCode: string;
|
fixedCssCode: string;
|
||||||
|
@ -1,11 +1,32 @@
|
|||||||
import type { BuildOptions } from "../BuildOptions";
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
urlPathname: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||||
fixedCssCode: string;
|
fixedCssCode: string;
|
||||||
@ -16,7 +37,10 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
|
|||||||
buildOptions.urlPathname === undefined
|
buildOptions.urlPathname === undefined
|
||||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||||
(...[, group]) => `url(\${url.resourcesPath}/build/${group})`
|
(...[, group]) =>
|
||||||
|
`url(${
|
||||||
|
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||||
|
})`
|
||||||
);
|
);
|
||||||
|
|
||||||
return { fixedCssCode };
|
return { fixedCssCode };
|
||||||
|
@ -17,7 +17,7 @@ export async function promptKeycloakVersion() {
|
|||||||
return { getLatestsSemVersionedTag };
|
return { getLatestsSemVersionedTag };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
console.log("Select Keycloak version?");
|
console.log("Initialize the directory with email template from which keycloak version?");
|
||||||
|
|
||||||
const tags = [
|
const tags = [
|
||||||
...(await getLatestsSemVersionedTag({
|
...(await getLatestsSemVersionedTag({
|
||||||
|
@ -1,55 +1,18 @@
|
|||||||
import { exec as execCallback } from "child_process";
|
import { exec as execCallback } from "child_process";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
|
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
||||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import { getProjectRoot } from "./getProjectRoot";
|
||||||
import { transformCodebase } from "./transformCodebase";
|
import { transformCodebase } from "./transformCodebase";
|
||||||
import { unzip, zip } from "./unzip";
|
import { unzip } from "./unzip";
|
||||||
|
|
||||||
const exec = promisify(execCallback);
|
const exec = promisify(execCallback);
|
||||||
|
|
||||||
function generateFileNameFromURL(params: {
|
function hash(s: string) {
|
||||||
url: string;
|
return createHash("sha256").update(s).digest("hex");
|
||||||
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) {
|
async function exists(path: string) {
|
||||||
@ -94,6 +57,8 @@ function readNpmConfig(): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
|
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(String(error), error);
|
||||||
|
|
||||||
if (String(error).includes("ENOWORKSPACES")) {
|
if (String(error).includes("ENOWORKSPACES")) {
|
||||||
assert(cwd !== pathSep);
|
assert(cwd !== pathSep);
|
||||||
|
|
||||||
@ -150,43 +115,14 @@ async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy"
|
|||||||
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
|
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadAndUnzip(
|
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
||||||
params: {
|
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||||
url: string;
|
|
||||||
destDirPath: string;
|
|
||||||
specificDirsToExtract?: string[];
|
|
||||||
preCacheTransform?: {
|
|
||||||
actionCacheId: string;
|
|
||||||
action: (params: { destDirPath: string }) => Promise<void>;
|
|
||||||
};
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
doUseCache: true;
|
|
||||||
projectDirPath: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
doUseCache: false;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
|
|
||||||
|
|
||||||
const zipFileBasename = generateFileNameFromURL({
|
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
|
||||||
url,
|
const projectRoot = getProjectRoot();
|
||||||
"preCacheTransform":
|
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
|
||||||
preCacheTransform === undefined
|
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
|
||||||
? undefined
|
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
|
||||||
: {
|
|
||||||
"actionCacheId": preCacheTransform.actionCacheId,
|
|
||||||
"actionFootprint": preCacheTransform.action.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheRoot = !rest.doUseCache
|
|
||||||
? `tmp_${Math.random().toString().slice(2, 12)}`
|
|
||||||
: pathJoin(process.env.XDG_CACHE_HOME ?? pathJoin(rest.projectDirPath, "node_modules", ".cache"), "keycloakify");
|
|
||||||
const zipFilePath = pathJoin(cacheRoot, `${zipFileBasename}.zip`);
|
|
||||||
const extractDirPath = pathJoin(cacheRoot, `tmp_unzip_${zipFileBasename}`);
|
|
||||||
|
|
||||||
if (!(await exists(zipFilePath))) {
|
if (!(await exists(zipFilePath))) {
|
||||||
const opts = await getFetchOptions();
|
const opts = await getFetchOptions();
|
||||||
@ -202,32 +138,12 @@ export async function downloadAndUnzip(
|
|||||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||||
assert(typeof response.body !== "undefined" && response.body != null);
|
assert(typeof response.body !== "undefined" && response.body != null);
|
||||||
await writeFile(zipFilePath, response.body);
|
await writeFile(zipFilePath, response.body);
|
||||||
|
|
||||||
if (specificDirsToExtract !== undefined || preCacheTransform !== undefined) {
|
|
||||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
|
||||||
|
|
||||||
await preCacheTransform?.action({
|
|
||||||
"destDirPath": extractDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
await unlink(zipFilePath);
|
|
||||||
|
|
||||||
await zip(extractDirPath, zipFilePath);
|
|
||||||
|
|
||||||
await rm(extractDirPath, { "recursive": true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await unzip(zipFilePath, extractDirPath);
|
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": extractDirPath,
|
"srcDirPath": extractDirPath,
|
||||||
"destDirPath": destDirPath
|
"destDirPath": destDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!rest.doUseCache) {
|
|
||||||
await rm(cacheRoot, { "recursive": true });
|
|
||||||
} else {
|
|
||||||
await rm(extractDirPath, { "recursive": true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import * as path from "path";
|
|||||||
import { crawl } from "./crawl";
|
import { crawl } from "./crawl";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
|
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string }) =>
|
||||||
| {
|
| {
|
||||||
modifiedSourceCode: Buffer;
|
modifiedSourceCode: Buffer;
|
||||||
newFileName?: string;
|
newFileName?: string;
|
||||||
@ -20,27 +20,26 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
|||||||
}))
|
}))
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
|
for (const file_relative_path of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
|
||||||
const filePath = path.join(srcDirPath, fileRelativePath);
|
const filePath = path.join(srcDirPath, file_relative_path);
|
||||||
|
|
||||||
const transformSourceCodeResult = transformSourceCode({
|
const transformSourceCodeResult = transformSourceCode({
|
||||||
"sourceCode": fs.readFileSync(filePath),
|
"sourceCode": fs.readFileSync(filePath),
|
||||||
filePath,
|
filePath
|
||||||
fileRelativePath
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transformSourceCodeResult === undefined) {
|
if (transformSourceCodeResult === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
|
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
|
||||||
"recursive": true
|
"recursive": true
|
||||||
});
|
});
|
||||||
|
|
||||||
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
|
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
|
||||||
modifiedSourceCode
|
modifiedSourceCode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import fsp from "node:fs/promises";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import yauzl from "yauzl";
|
import yauzl from "yauzl";
|
||||||
import yazl from "yazl";
|
|
||||||
import stream from "node:stream";
|
import stream from "node:stream";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
@ -20,16 +19,11 @@ async function pathExists(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlings of non posix path is not implemented correctly
|
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
|
||||||
// it work by coincidence. Don't have the time to fix but it should be fixed.
|
// add trailing slash to unzipSubPath and targetFolder
|
||||||
export async function unzip(file: string, targetFolder: string, specificDirsToExtract?: string[]) {
|
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
|
||||||
specificDirsToExtract = specificDirsToExtract?.map(dirPath => {
|
unzipSubPath += "/";
|
||||||
if (!dirPath.endsWith("/") || !dirPath.endsWith("\\")) {
|
}
|
||||||
dirPath += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirPath;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
|
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
|
||||||
targetFolder += "/";
|
targetFolder += "/";
|
||||||
@ -48,17 +42,15 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
|||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
|
|
||||||
zipfile.on("entry", async entry => {
|
zipfile.on("entry", async entry => {
|
||||||
if (specificDirsToExtract !== undefined) {
|
if (unzipSubPath) {
|
||||||
const dirPath = specificDirsToExtract.find(dirPath => entry.fileName.startsWith(dirPath));
|
|
||||||
|
|
||||||
// Skip files outside of the unzipSubPath
|
// Skip files outside of the unzipSubPath
|
||||||
if (dirPath === undefined) {
|
if (!entry.fileName.startsWith(unzipSubPath)) {
|
||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the unzipSubPath from the file name
|
// Remove the unzipSubPath from the file name
|
||||||
entry.fileName = entry.fileName.substring(dirPath.length);
|
entry.fileName = entry.fileName.substring(unzipSubPath.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = path.join(targetFolder, entry.fileName);
|
const target = path.join(targetFolder, entry.fileName);
|
||||||
@ -85,8 +77,6 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.mkdir(path.dirname(target), { "recursive": true });
|
|
||||||
|
|
||||||
await pipeline(readStream, fs.createWriteStream(target));
|
await pipeline(readStream, fs.createWriteStream(target));
|
||||||
|
|
||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
@ -100,42 +90,3 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This code was directly copied from ChatGPT and appears to function as expected.
|
|
||||||
// However, confidence in its complete accuracy and robustness is limited.
|
|
||||||
export async function zip(sourceFolder: string, targetZip: string) {
|
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
|
||||||
const zipfile = new yazl.ZipFile();
|
|
||||||
const files: string[] = [];
|
|
||||||
|
|
||||||
// Recursive function to explore directories and their subdirectories
|
|
||||||
async function exploreDir(dir: string) {
|
|
||||||
const dirContent = await fsp.readdir(dir);
|
|
||||||
for (const file of dirContent) {
|
|
||||||
const filePath = path.join(dir, file);
|
|
||||||
const stat = await fsp.stat(filePath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
await exploreDir(filePath);
|
|
||||||
} else if (stat.isFile()) {
|
|
||||||
files.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collecting all files to be zipped
|
|
||||||
await exploreDir(sourceFolder);
|
|
||||||
|
|
||||||
// Adding files to zip
|
|
||||||
for (const file of files) {
|
|
||||||
const relativePath = path.relative(sourceFolder, file);
|
|
||||||
zipfile.addFile(file, relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
zipfile.outputStream
|
|
||||||
.pipe(fs.createWriteStream(targetZip))
|
|
||||||
.on("close", () => resolve())
|
|
||||||
.on("error", err => reject(err)); // Listen to error events
|
|
||||||
|
|
||||||
zipfile.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { useReducer, useEffect } from "react";
|
import { useReducer, useEffect } from "react";
|
||||||
import { headInsert } from "keycloakify/tools/headInsert";
|
import { headInsert } from "keycloakify/tools/headInsert";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
|
||||||
export function usePrepareTemplate(params: {
|
export function usePrepareTemplate(params: {
|
||||||
doFetchDefaultThemeResources: boolean;
|
doFetchDefaultThemeResources: boolean;
|
||||||
|
stylesCommon?: string[];
|
||||||
styles?: string[];
|
styles?: string[];
|
||||||
scripts?: string[];
|
scripts?: string[];
|
||||||
|
url: {
|
||||||
|
resourcesCommonPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
};
|
||||||
htmlClassName: string | undefined;
|
htmlClassName: string | undefined;
|
||||||
bodyClassName: string | undefined;
|
bodyClassName: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName } = params;
|
const { doFetchDefaultThemeResources, stylesCommon = [], styles = [], url, scripts = [], htmlClassName, bodyClassName } = params;
|
||||||
|
|
||||||
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
||||||
|
|
||||||
@ -25,18 +31,23 @@ export function usePrepareTemplate(params: {
|
|||||||
(async () => {
|
(async () => {
|
||||||
const prLoadedArray: Promise<void>[] = [];
|
const prLoadedArray: Promise<void>[] = [];
|
||||||
|
|
||||||
styles.reverse().forEach(href => {
|
[
|
||||||
const { prLoaded, remove } = headInsert({
|
...stylesCommon.map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||||
"type": "css",
|
...styles.map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||||
"position": "prepend",
|
]
|
||||||
href
|
.reverse()
|
||||||
|
.forEach(href => {
|
||||||
|
const { prLoaded, remove } = headInsert({
|
||||||
|
"type": "css",
|
||||||
|
"position": "prepend",
|
||||||
|
href
|
||||||
|
});
|
||||||
|
|
||||||
|
removeArray.push(remove);
|
||||||
|
|
||||||
|
prLoadedArray.push(prLoaded);
|
||||||
});
|
});
|
||||||
|
|
||||||
removeArray.push(remove);
|
|
||||||
|
|
||||||
prLoadedArray.push(prLoaded);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(prLoadedArray);
|
await Promise.all(prLoadedArray);
|
||||||
|
|
||||||
if (isUnmounted) {
|
if (isUnmounted) {
|
||||||
@ -46,10 +57,10 @@ export function usePrepareTemplate(params: {
|
|||||||
setReady();
|
setReady();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
scripts.forEach(src => {
|
scripts.forEach(relativePath => {
|
||||||
const { remove } = headInsert({
|
const { remove } = headInsert({
|
||||||
"type": "javascript",
|
"type": "javascript",
|
||||||
src
|
"src": pathJoin(url.resourcesPath, relativePath)
|
||||||
});
|
});
|
||||||
|
|
||||||
removeArray.push(remove);
|
removeArray.push(remove);
|
||||||
|
@ -31,14 +31,15 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
|
|
||||||
const { isReady } = usePrepareTemplate({
|
const { isReady } = usePrepareTemplate({
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||||
"styles": [
|
url,
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
"stylesCommon": [
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
"node_modules/patternfly/dist/css/patternfly.min.css",
|
||||||
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
|
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||||
`${url.resourcesPath}/css/login.css`
|
"lib/zocial/zocial.css"
|
||||||
],
|
],
|
||||||
|
"styles": ["css/login.css"],
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
"htmlClassName": getClassName("kcHtmlClass"),
|
||||||
"bodyClassName": getClassName("kcBodyClass")
|
"bodyClassName": undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
|
@ -21,7 +21,6 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ClassKey =
|
export type ClassKey =
|
||||||
| "kcBodyClass"
|
|
||||||
| "kcHtmlClass"
|
| "kcHtmlClass"
|
||||||
| "kcLoginClass"
|
| "kcLoginClass"
|
||||||
| "kcHeaderClass"
|
| "kcHeaderClass"
|
||||||
|
@ -145,7 +145,7 @@ export declare namespace KcContext {
|
|||||||
rememberMe?: string;
|
rememberMe?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
usernameHidden?: boolean;
|
usernameEditDisabled: boolean;
|
||||||
social: {
|
social: {
|
||||||
displayInfo: boolean;
|
displayInfo: boolean;
|
||||||
providers?: {
|
providers?: {
|
||||||
|
@ -234,6 +234,10 @@ export const kcContextCommonMock: KcContext.Common = {
|
|||||||
"clientId": "myApp"
|
"clientId": "myApp"
|
||||||
},
|
},
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
|
"message": {
|
||||||
|
"type": "success",
|
||||||
|
"summary": "This is a test message"
|
||||||
|
},
|
||||||
"isAppInitiatedAction": false
|
"isAppInitiatedAction": false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -260,7 +264,7 @@ export const kcContextMocks = [
|
|||||||
"social": {
|
"social": {
|
||||||
"displayInfo": true
|
"displayInfo": true
|
||||||
},
|
},
|
||||||
"usernameHidden": false,
|
"usernameEditDisabled": false,
|
||||||
"login": {},
|
"login": {},
|
||||||
"registrationDisabled": false
|
"registrationDisabled": false
|
||||||
}),
|
}),
|
||||||
|
@ -3,7 +3,6 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
|
|||||||
|
|
||||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||||
"defaultClasses": {
|
"defaultClasses": {
|
||||||
"kcBodyClass": undefined,
|
|
||||||
"kcHtmlClass": "login-pf",
|
"kcHtmlClass": "login-pf",
|
||||||
"kcLoginClass": "login-pf-page",
|
"kcLoginClass": "login-pf-page",
|
||||||
"kcContentWrapperClass": "row",
|
"kcContentWrapperClass": "row",
|
||||||
|
@ -14,7 +14,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
|
|
||||||
const { social, realm, url, usernameHidden, login, auth, registrationDisabled } = kcContext;
|
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -66,37 +66,40 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
{realm.password && (
|
{realm.password && (
|
||||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={getClassName("kcFormGroupClass")}>
|
||||||
{!usernameHidden &&
|
{(() => {
|
||||||
(() => {
|
const label = !realm.loginWithEmailAllowed
|
||||||
const label = !realm.loginWithEmailAllowed
|
? "username"
|
||||||
? "username"
|
: realm.registrationEmailAsUsername
|
||||||
: realm.registrationEmailAsUsername
|
? "email"
|
||||||
? "email"
|
: "usernameOrEmail";
|
||||||
: "usernameOrEmail";
|
|
||||||
|
|
||||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
|
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
|
||||||
{msg(label)}
|
{msg(label)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
id={autoCompleteHelper}
|
id={autoCompleteHelper}
|
||||||
className={getClassName("kcInputClass")}
|
className={getClassName("kcInputClass")}
|
||||||
//NOTE: This is used by Google Chrome auto fill so we use it to tell
|
//NOTE: This is used by Google Chrome auto fill so we use it to tell
|
||||||
//the browser how to pre fill the form but before submit we put it back
|
//the browser how to pre fill the form but before submit we put it back
|
||||||
//to username because it is what keycloak expects.
|
//to username because it is what keycloak expects.
|
||||||
name={autoCompleteHelper}
|
name={autoCompleteHelper}
|
||||||
defaultValue={login.username ?? ""}
|
defaultValue={login.username ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus={true}
|
{...(usernameEditDisabled
|
||||||
autoComplete="off"
|
? { "disabled": true }
|
||||||
/>
|
: {
|
||||||
</>
|
"autoFocus": true,
|
||||||
);
|
"autoComplete": "off"
|
||||||
})()}
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={getClassName("kcFormGroupClass")}>
|
||||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||||
@ -113,7 +116,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
</div>
|
</div>
|
||||||
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
|
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
|
||||||
<div id="kc-form-options">
|
<div id="kc-form-options">
|
||||||
{realm.rememberMe && !usernameHidden && (
|
{realm.rememberMe && !usernameEditDisabled && (
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { headInsert } from "keycloakify/tools/headInsert";
|
import { headInsert } from "keycloakify/tools/headInsert";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
@ -23,7 +24,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
|
|
||||||
const { prLoaded, remove } = headInsert({
|
const { prLoaded, remove } = headInsert({
|
||||||
"type": "javascript",
|
"type": "javascript",
|
||||||
"src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
|
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -22,11 +22,10 @@ const meta: ComponentMeta<any> = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
export const Default = () => <PageStory />;
|
export const Default = () => <PageStory />;
|
||||||
|
export const WithNoMessage = () => (
|
||||||
export const WithMessage = () => (
|
|
||||||
<PageStory
|
<PageStory
|
||||||
kcContext={{
|
kcContext={{
|
||||||
message: { type: "success", summary: "This is a test message" }
|
message: undefined
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -74,12 +74,8 @@ export const WithPresetUsername = () => (
|
|||||||
export const WithImmutablePresetUsername = () => (
|
export const WithImmutablePresetUsername = () => (
|
||||||
<PageStory
|
<PageStory
|
||||||
kcContext={{
|
kcContext={{
|
||||||
auth: {
|
login: { username: "max.mustermann@mail.com" },
|
||||||
attemptedUsername: "max.mustermann@mail.com",
|
usernameEditDisabled: true
|
||||||
showUsername: true
|
|
||||||
},
|
|
||||||
usernameHidden: true,
|
|
||||||
message: { type: "info", summary: "Please re-authenticate to continue" }
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
import { readPaths } from "keycloakify/bin/keycloakify/generateTheme/readStaticResourcesUsage";
|
|
||||||
import { same } from "evt/tools/inDepth/same";
|
|
||||||
import { expect, it, describe } from "vitest";
|
|
||||||
|
|
||||||
describe("Ensure it's able to extract used Keycloak resources", () => {
|
|
||||||
const expectedPaths = {
|
|
||||||
"resourcesCommonFilePaths": [
|
|
||||||
"node_modules/patternfly/dist/css/patternfly.min.css",
|
|
||||||
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
|
||||||
"lib/zocial/zocial.css",
|
|
||||||
"node_modules/jquery/dist/jquery.min.js"
|
|
||||||
],
|
|
||||||
"resourcesFilePaths": ["css/login.css"]
|
|
||||||
};
|
|
||||||
|
|
||||||
it("works with coding style n°1", () => {
|
|
||||||
const paths = readPaths({
|
|
||||||
"rawSourceFile": `
|
|
||||||
const { isReady } = usePrepareTemplate({
|
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
|
||||||
"styles": [
|
|
||||||
\`\${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css\`,
|
|
||||||
\`\${
|
|
||||||
url.resourcesCommonPath
|
|
||||||
}/node_modules/patternfly/dist/css/patternfly-additions.min.css\`,
|
|
||||||
\`\${resourcesCommonPath }/lib/zocial/zocial.css\`,
|
|
||||||
\`\${url.resourcesPath}/css/login.css\`
|
|
||||||
],
|
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
|
||||||
"bodyClassName": undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
const { prLoaded, remove } = headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
"src": \`\${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js\`
|
|
||||||
});
|
|
||||||
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(same(paths, expectedPaths)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works with coding style n°2", () => {
|
|
||||||
const paths = readPaths({
|
|
||||||
"rawSourceFile": `
|
|
||||||
|
|
||||||
const { isReady } = usePrepareTemplate({
|
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
|
||||||
"styles": [
|
|
||||||
url.resourcesCommonPath + "/node_modules/patternfly/dist/css/patternfly.min.css",
|
|
||||||
url.resourcesCommonPath + '/node_modules/patternfly/dist/css/patternfly-additions.min.css',
|
|
||||||
url.resourcesCommonPath
|
|
||||||
+ "/lib/zocial/zocial.css",
|
|
||||||
url.resourcesPath +
|
|
||||||
'/css/login.css'
|
|
||||||
],
|
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
|
||||||
"bodyClassName": undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
const { prLoaded, remove } = headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
"src": kcContext.url.resourcesCommonPath + "/node_modules/jquery/dist/jquery.min.js\"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(paths);
|
|
||||||
console.log(expectedPaths);
|
|
||||||
|
|
||||||
expect(same(paths, expectedPaths)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works with coding style n°3", () => {
|
|
||||||
const paths = readPaths({
|
|
||||||
"rawSourceFile": `
|
|
||||||
|
|
||||||
const { isReady } = usePrepareTemplate({
|
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
|
||||||
"styles": [
|
|
||||||
path.join(resourcesCommonPath,"/node_modules/patternfly/dist/css/patternfly.min.css"),
|
|
||||||
path.join(url.resourcesCommonPath, '/node_modules/patternfly/dist/css/patternfly-additions.min.css'),
|
|
||||||
path.join(url.resourcesCommonPath,
|
|
||||||
"/lib/zocial/zocial.css"),
|
|
||||||
pathJoin(
|
|
||||||
url.resourcesPath,
|
|
||||||
'css/login.css'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
|
||||||
"bodyClassName": undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
const { prLoaded, remove } = headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
"src": path.join(kcContext.url.resourcesCommonPath, "/node_modules/jquery/dist/jquery.min.js")
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(same(paths, expectedPaths)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
@ -32,14 +32,13 @@ describe("bin/js-transforms", () => {
|
|||||||
908:"67c9ed2c"
|
908:"67c9ed2c"
|
||||||
}[e]+".chunk.css"
|
}[e]+".chunk.css"
|
||||||
}
|
}
|
||||||
|
|
||||||
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
|
||||||
|
|
||||||
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
|
||||||
`;
|
`;
|
||||||
it("transforms standalone code properly", () => {
|
it("transforms standalone code properly", () => {
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": jsCodeUntransformed
|
"jsCode": jsCodeUntransformed,
|
||||||
|
"buildOptions": {
|
||||||
|
"isStandalone": true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedJsCodeExpected = `
|
const fixedJsCodeExpected = `
|
||||||
@ -56,11 +55,11 @@ describe("bin/js-transforms", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__webpack_require__[(function (){
|
__webpack_require__[(function (){
|
||||||
var pd = Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||||
if( pd === undefined || pd.configurable ){
|
if( pd === undefined || pd.configurable ){
|
||||||
Object.defineProperty(__webpack_require__, "p", {
|
Object.defineProperty(__webpack_require__, "p", {
|
||||||
get: function() { return window.kcContext.url.resourcesPath; },
|
get: function() { return window.kcContext.url.resourcesPath; },
|
||||||
set: function() {}
|
set: function (){}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return "u";
|
return "u";
|
||||||
@ -73,11 +72,11 @@ describe("bin/js-transforms", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t[(function (){
|
t[(function (){
|
||||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
var pd= Object.getOwnPropertyDescriptor(t, "p");
|
||||||
if( pd === undefined || pd.configurable ){
|
if( pd === undefined || pd.configurable ){
|
||||||
Object.defineProperty(t, "p", {
|
Object.defineProperty(t, "p", {
|
||||||
get: function() { return window.kcContext.url.resourcesPath; },
|
get: function() { return window.kcContext.url.resourcesPath; },
|
||||||
set: function() {}
|
set: function (){}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return "miniCssF";
|
return "miniCssF";
|
||||||
@ -87,28 +86,67 @@ describe("bin/js-transforms", () => {
|
|||||||
908:"67c9ed2c"
|
908:"67c9ed2c"
|
||||||
} [e] + ".chunk.css"
|
} [e] + ".chunk.css"
|
||||||
}
|
}
|
||||||
|
|
||||||
n[(function(){
|
`;
|
||||||
var pd = Object.getOwnPropertyDescriptor(n, "p");
|
|
||||||
|
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||||
|
});
|
||||||
|
it("transforms external app code properly", () => {
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": jsCodeUntransformed,
|
||||||
|
"buildOptions": {
|
||||||
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixedJsCodeExpected = `
|
||||||
|
function f() {
|
||||||
|
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
|
||||||
|
3: "0664cdc0"
|
||||||
|
}[e] + ".chunk.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameAsF() {
|
||||||
|
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
|
||||||
|
3: "0664cdc0"
|
||||||
|
}[e] + ".chunk.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
__webpack_require__[(function (){
|
||||||
|
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||||
if( pd === undefined || pd.configurable ){
|
if( pd === undefined || pd.configurable ){
|
||||||
Object.defineProperty(n, "p", {
|
var p= "";
|
||||||
get: function() { return window.kcContext.url.resourcesPath; },
|
Object.defineProperty(__webpack_require__, "p", {
|
||||||
set: function() {}
|
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
|
||||||
|
set: function (value){ p = value; }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return "u";
|
return "u";
|
||||||
})()] = e => "/build/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
})()] = function(e) {
|
||||||
|
return "static/js/" + e + "." + {
|
||||||
t[(function(){
|
147: "6c5cee76",
|
||||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
787: "8da10fcf",
|
||||||
|
922: "be170a73"
|
||||||
|
} [e] + ".chunk.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
t[(function (){
|
||||||
|
var pd= Object.getOwnPropertyDescriptor(t, "p");
|
||||||
if( pd === undefined || pd.configurable ){
|
if( pd === undefined || pd.configurable ){
|
||||||
|
var p= "";
|
||||||
Object.defineProperty(t, "p", {
|
Object.defineProperty(t, "p", {
|
||||||
get: function() { return window.kcContext.url.resourcesPath; },
|
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
|
||||||
set: function() {}
|
set: function (value){ p = value; }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return "miniCssF";
|
return "miniCssF";
|
||||||
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
})()] = function(e) {
|
||||||
|
return "static/css/" + e + "." + {
|
||||||
|
164:"dcfd7749",
|
||||||
|
908:"67c9ed2c"
|
||||||
|
} [e] + ".chunk.css"
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||||
@ -266,6 +304,7 @@ describe("bin/css-inline-transforms", () => {
|
|||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
|
"isStandalone": true,
|
||||||
"urlPathname": undefined
|
"urlPathname": undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -305,6 +344,53 @@ describe("bin/css-inline-transforms", () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
||||||
|
});
|
||||||
|
it("transforms css for external app properly", () => {
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
cssCode,
|
||||||
|
"buildOptions": {
|
||||||
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev",
|
||||||
|
"urlPathname": undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixedCssCodeExpected = `
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-regular-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-medium-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-semibold-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-bold-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -344,6 +430,7 @@ describe("bin/css-inline-transforms", () => {
|
|||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
|
"isStandalone": true,
|
||||||
"urlPathname": "/x/y/z/"
|
"urlPathname": "/x/y/z/"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -383,6 +470,53 @@ describe("bin/css-inline-transforms", () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
||||||
|
});
|
||||||
|
it("transforms css for external app properly", () => {
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
cssCode,
|
||||||
|
"buildOptions": {
|
||||||
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev",
|
||||||
|
"urlPathname": "/x/y/z/"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixedCssCodeExpected = `
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-regular-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-medium-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-semibold-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Work Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-bold-webfont.woff2)
|
||||||
|
format("woff2");
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,8 +12,7 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
|
|||||||
async function setupSampleReactProject(destDir: string) {
|
async function setupSampleReactProject(destDir: string) {
|
||||||
await downloadAndUnzip({
|
await downloadAndUnzip({
|
||||||
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||||
"destDirPath": destDir,
|
"destDirPath": destDir
|
||||||
"doUseCache": false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let parsedPackageJson: Record<string, unknown> = {};
|
let parsedPackageJson: Record<string, unknown> = {};
|
||||||
@ -52,19 +51,17 @@ describe("Sample Project", () => {
|
|||||||
await setupSampleReactProject(sampleReactProjectDirPath);
|
await setupSampleReactProject(sampleReactProjectDirPath);
|
||||||
await initializeEmailTheme();
|
await initializeEmailTheme();
|
||||||
|
|
||||||
const projectDirPath = process.cwd();
|
|
||||||
|
|
||||||
const destDirPath = pathJoin(
|
const destDirPath = pathJoin(
|
||||||
readBuildOptions({
|
readBuildOptions({
|
||||||
"processArgv": ["--silent"],
|
"processArgv": ["--silent"],
|
||||||
projectDirPath
|
"projectDirPath": process.cwd()
|
||||||
}).keycloakifyBuildDirPath,
|
}).keycloakifyBuildDirPath,
|
||||||
"src",
|
"src",
|
||||||
"main",
|
"main",
|
||||||
"resources",
|
"resources",
|
||||||
"theme"
|
"theme"
|
||||||
);
|
);
|
||||||
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
|
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", "isSilent": false });
|
||||||
},
|
},
|
||||||
{ timeout: 90000 }
|
{ timeout: 90000 }
|
||||||
);
|
);
|
||||||
@ -80,19 +77,17 @@ describe("Sample Project", () => {
|
|||||||
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
|
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
|
||||||
await initializeEmailTheme();
|
await initializeEmailTheme();
|
||||||
|
|
||||||
const projectDirPath = process.cwd();
|
|
||||||
|
|
||||||
const destDirPath = pathJoin(
|
const destDirPath = pathJoin(
|
||||||
readBuildOptions({
|
readBuildOptions({
|
||||||
"processArgv": ["--silent"],
|
"processArgv": ["--silent"],
|
||||||
projectDirPath
|
"projectDirPath": process.cwd()
|
||||||
}).keycloakifyBuildDirPath,
|
}).keycloakifyBuildDirPath,
|
||||||
"src",
|
"src",
|
||||||
"main",
|
"main",
|
||||||
"resources",
|
"resources",
|
||||||
"theme"
|
"theme"
|
||||||
);
|
);
|
||||||
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
|
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", "isSilent": false });
|
||||||
},
|
},
|
||||||
{ timeout: 90000 }
|
{ timeout: 90000 }
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@ import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
|
|||||||
export async function setupSampleReactProject(destDirPath: string) {
|
export async function setupSampleReactProject(destDirPath: string) {
|
||||||
await downloadAndUnzip({
|
await downloadAndUnzip({
|
||||||
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||||
"destDirPath": destDirPath,
|
"destDirPath": destDirPath
|
||||||
"doUseCache": false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user