Compare commits

..

68 Commits

Author SHA1 Message Date
a9231e2ed8 Bump version 2024-02-04 06:31:10 +01:00
5f4669a7a6 Merge pull request #502 from giorgoslytos/fix/login-otp-radio-inputs 2024-02-03 10:53:58 +01:00
2a07f7151d Merge pull request #503 from keycloakify/all-contributors/add-giorgoslytos
docs: add giorgoslytos as a contributor for code
2024-02-03 08:13:16 +01:00
b6ecff2dd3 docs: update .all-contributorsrc [skip ci] 2024-02-03 07:11:46 +00:00
83df27ec99 docs: update README.md [skip ci] 2024-02-03 07:11:45 +00:00
ca255985c0 fix: radio inputs on login-otp page 2024-02-02 14:35:14 +02:00
82f34c38f6 Bump version 2024-02-01 06:22:54 +01:00
694b4c8027 Reintroduce doBuildRetrocompatAccountTheme (for now) and fix multiple things in the account default theme 2024-02-01 06:22:33 +01:00
33b7bb6184 Bump version 2024-01-26 04:09:54 +01:00
7d9130b2af Remove the doBuildRetrocompatAccountTheme and the retrocompat_ option in dropdown 2024-01-26 03:39:48 +01:00
482d71743b Bump version 2024-01-26 02:03:06 +01:00
1db37a4727 Do not include retrocompat_ in META-INF when using doBuildRetrocompatAccountTheme false 2024-01-26 02:02:45 +01:00
194d16ff91 Bump version 2024-01-26 00:56:45 +01:00
b1e2284c0e Fix condition for displaying info in login page 2024-01-26 00:55:50 +01:00
70d1aa70a3 Bump version 2024-01-18 18:09:59 +01:00
3b17d6e0ab Merge pull request #496 from keycloakify/all-contributors/add-Moulyy
docs: add Moulyy as a contributor for code
2024-01-18 18:09:10 +01:00
9a5819b93b docs: update .all-contributorsrc [skip ci] 2024-01-18 16:45:36 +00:00
a260cd67b0 docs: update README.md [skip ci] 2024-01-18 16:45:35 +00:00
64111fb0ec Merge pull request #495 from Moulyy/fix/add_password_visibility_classkey
Add "kcInputGroup" in ClassKey type definition and useGetClassName
2024-01-18 17:43:48 +01:00
faf2be23d9 Add "kcInputGroup" in ClassKey type definition and the patternfly class associated in useGetClassName 2024-01-18 17:31:16 +01:00
0eb4a6a315 Merge pull request #494 from alexted/patch-1
Update README.md
2024-01-18 16:59:17 +01:00
85673250ed Update README.md
extra "v" deleted  in several places of changelog highlights
2024-01-18 17:48:10 +02:00
09daa741ce Install pnpm if needed 2024-01-18 01:02:14 +01:00
55e2379aab Bump version 2024-01-18 00:57:02 +01:00
9937977203 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-18 00:56:26 +01:00
c897e7491a Use message of Keycloak version 23.0.4 #493 2024-01-18 00:54:09 +01:00
0a74a95283 Bump version 2024-01-15 11:02:00 +01:00
74ef2c3dff Merge pull request #489 from law108000/main
refactor: point the import statement to keycloakify instead of internal
2024-01-15 11:01:14 +01:00
9976dfacc0 Merge pull request #491 from keycloakify/all-contributors/add-law108000
docs: add law108000 as a contributor for code
2024-01-15 10:59:17 +01:00
659f8ddc7a docs: update .all-contributorsrc [skip ci] 2024-01-15 09:56:30 +00:00
9e4cc2ae57 docs: update README.md [skip ci] 2024-01-15 09:56:29 +00:00
a27d78fcdf refactor: point the import statement to keycloakify instead of internal
align import statement with other page templates
2024-01-15 14:03:01 +08:00
e507435bcb Bump version 2024-01-10 13:01:23 +01:00
d5f234909f Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-10 12:43:11 +01:00
c17f721625 Bump version 2024-01-10 12:43:04 +01:00
600705130f #483: Enable the Template prop not to be lazy 2024-01-10 12:42:48 +01:00
5c5dce1422 Bump version 2023-12-18 13:19:54 +01:00
53585bf2f0 Merge pull request #476 from BlackVoid/feature/document-title
Fixes #473: Set document title using the "loginTitle" translation
2023-12-18 13:18:51 +01:00
116f88a503 Fixes #473: Set document title using the "loginTitle" translation 2023-12-18 11:56:37 +01:00
aaba8cd2c7 Bump version 2023-12-14 15:34:13 +01:00
b67aeb0d3a Fix tests #471 2023-12-14 15:33:57 +01:00
f620562d68 docs: update .all-contributorsrc [skip ci] 2023-12-14 15:09:10 +01:00
5231d0eaa1 docs: update README.md [skip ci] 2023-12-14 15:09:09 +01:00
cb470e3573 Handle CSS with multiple urls 2023-12-12 14:51:09 +00:00
0a0f90aa2e Bump version 2023-12-04 14:10:25 +01:00
635207d12c Generate a README alongside the jars to indicate why there's two jar file 2023-12-04 14:10:10 +01:00
5e4a829413 Bump version 2023-12-01 00:54:36 +01:00
b13b3fd92e Mention the nessesity to use retrocompat- with older Keycloak versions 2023-12-01 00:54:19 +01:00
564dc8e6f1 Bump version 2023-12-01 00:03:58 +01:00
6e4cced8c6 Rename original- to retrocompat- 2023-12-01 00:03:44 +01:00
29a4a5027c Bump version 2023-11-30 22:37:13 +01:00
ee327448b4 Delete original- jar 2023-11-30 22:36:58 +01:00
d078960c5c Bump version 2023-11-30 18:53:17 +01:00
2e8cd375fc Merge pull request #462 from BlackVoid/fix/client-attributes
Fixes #460: Fixes KcContext to contain attributes for client object
2023-11-30 18:52:26 +01:00
1f6751cb01 Fixes #460: Fixes KcContext to contain attributes for client object 2023-11-30 17:17:58 +01:00
3cca4e31cd Merge pull request #463 from keycloakify/all-contributors/add-BlackVoid
docs: add BlackVoid as a contributor for code
2023-11-30 17:10:29 +01:00
b93902800c docs: update .all-contributorsrc [skip ci] 2023-11-30 16:06:55 +00:00
70f6bb3fda docs: update README.md [skip ci] 2023-11-30 16:06:54 +00:00
c075cb6311 Merge pull request #461 from BlackVoid/fix/template
Fixes #459: Use Template from props
2023-11-30 17:03:36 +01:00
d7db85b062 Fixes #459: Use Template from props 2023-11-30 16:29:53 +01:00
b442e7d958 Merge pull request #457 from keycloakify/all-contributors/add-xgp
docs: add xgp as a contributor for code
2023-11-28 13:06:04 +01:00
a495ae637f docs: update .all-contributorsrc [skip ci] 2023-11-28 12:05:49 +00:00
94748a96a9 docs: update README.md [skip ci] 2023-11-28 12:05:48 +00:00
7657429054 Release v9 2023-11-28 12:53:37 +01:00
2ff6dbf975 Update README 2023-11-28 12:53:10 +01:00
4f34628c14 Merge pull request #414 from keycloakify/v9
v9: Support Keycloak 22 and decoupling account / login builtin resources
2023-11-28 12:18:07 +01:00
6ff2111cee #389 https://github.com/p2-inc/keycloak-account-v1/issues/3 2023-11-28 12:17:16 +01:00
85957980f6 update CI 2023-11-26 16:24:57 +01:00
48 changed files with 342 additions and 4152 deletions

View File

@ -186,6 +186,60 @@
"contributions": [
"code"
]
},
{
"login": "xgp",
"name": "Garth",
"avatar_url": "https://avatars.githubusercontent.com/u/244253?v=4",
"profile": "https://github.com/xgp",
"contributions": [
"code"
]
},
{
"login": "BlackVoid",
"name": "Felix Gustavsson",
"avatar_url": "https://avatars.githubusercontent.com/u/673720?v=4",
"profile": "https://github.com/BlackVoid",
"contributions": [
"code"
]
},
{
"login": "msiemens",
"name": "Markus Siemens",
"avatar_url": "https://avatars.githubusercontent.com/u/1873922?v=4",
"profile": "https://m-siemens.de/",
"contributions": [
"code"
]
},
{
"login": "law108000",
"name": "Rlok",
"avatar_url": "https://avatars.githubusercontent.com/u/8112024?v=4",
"profile": "https://github.com/law108000",
"contributions": [
"code"
]
},
{
"login": "Moulyy",
"name": "Moulyy",
"avatar_url": "https://avatars.githubusercontent.com/u/115405804?v=4",
"profile": "https://github.com/Moulyy",
"contributions": [
"code"
]
},
{
"login": "giorgoslytos",
"name": "giorgoslytos",
"avatar_url": "https://avatars.githubusercontent.com/u/50946162?v=4",
"profile": "https://github.com/giorgoslytos",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -3,7 +3,9 @@ on:
push:
branches:
- main
- v*
- v5
- v6
- v7
pull_request:
branches:
- main

View File

@ -36,24 +36,12 @@
<p align="center">
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
<br/>
<br/>
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
</p>
> Whether or not React is your preferred framework, Keycloakify
> offers a solid option for building Keycloak themes.
> It's not just a convenient way to create a Keycloak theme
> when using React; it's a well-regarded solution that many
> developers appreciate.
> 📣 🛑 Account themes generated by Keycloakify are not currently compatible with Keycloak 22.
> We are working on a solution. [Follow progress](https://github.com/keycloakify/keycloakify/issues/389).
> **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
> merged into Keycloak! 🥳 Credit to @xgp. We are now waiting for a new Keycloak release to be published.
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. (Well except for Keycloak 22's Account theme obviously but this was hopefully a one time debacle)
To understand the basis of my confidence in this, you can [visit this discussion thread where I've explained in detail](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791).
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
## Sponsor 👼
@ -119,6 +107,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xgp"><img src="https://avatars.githubusercontent.com/u/244253?v=4?s=100" width="100px;" alt="Garth"/><br /><sub><b>Garth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=xgp" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BlackVoid"><img src="https://avatars.githubusercontent.com/u/673720?v=4?s=100" width="100px;" alt="Felix Gustavsson"/><br /><sub><b>Felix Gustavsson</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=BlackVoid" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giorgoslytos"><img src="https://avatars.githubusercontent.com/u/50946162?v=4?s=100" width="100px;" alt="giorgoslytos"/><br /><sub><b>giorgoslytos</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=giorgoslytos" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@ -130,10 +126,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
## v9.0
## 9.0
Restore support for Keycloak 22.1 and up! Huge thanks to @xgp without whom this wouldn't have been possible.
Big thanks also to @ssilvert from the Keycloak team for being so open to discussion and merging [@xgp's PR](https://github.com/keycloak/keycloak/pull/22317).
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
### Breaking changes
@ -264,79 +259,79 @@ Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
## v5.6.0
## 5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## v5.3.0
## 5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
## v5.0.0
## 5.0.0
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
New i18n system.
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
## v4.10.0
## 4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## v4.8.0
## 4.8.0
[Email template customization.](#email-template-customization)
## v4.7.4
## 4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## v4.7.2
## 4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
## 4.7.0
Register with user profile enabled: Out of the box `options` validator support.
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
## v4.6.0
## 4.6.0
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
## v4.5.3
## 4.5.3
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
with this new method your theme wont break on minor Keycloakify update.
## v4.3.0
## 4.3.0
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
Every time a page is added it's a breaking change for non CSS-only theme.
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4
## 4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## v3
## 3
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
## v2.5
## 2.5
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2
## 2
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "9.0.0-rc.1",
"version": "9.3.1",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",

View File

@ -17,7 +17,7 @@ const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "21.0.1";
const keycloakVersion = "23.0.4";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");

View File

@ -1,9 +1,9 @@
import type { LazyExoticComponent } from "react";
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
export type PageProps<KcContext, I18nExtended extends I18n> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

View File

@ -27,6 +27,51 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
fix_account_css: {
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
if (!fs.existsSync(accountCssFilePath)) {
break fix_account_css;
}
fs.writeFileSync(
accountCssFilePath,
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
);
}
fix_account_topt: {
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
if (!fs.existsSync(totpFtlFilePath)) {
break fix_account_topt;
}
fs.writeFileSync(
totpFtlFilePath,
Buffer.from(
fs
.readFileSync(totpFtlFilePath)
.toString("utf8")
.replace(
[
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>"
].join("\n"),
[
" <#if totp.policy.supportedApplications?has_content>",
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>",
" </#if>"
].join("\n")
),
"utf8"
)
);
}
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
@ -55,7 +100,18 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
break install_and_move_to_common_resources_generated_in_keycloak_v2;
}
child_process.execSync("npm install", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
if (packageManager === "pnpm") {
try {
child_process.execSync(`which pnpm`);
} catch {
console.log(`Installing pnpm globally`);
child_process.execSync(`npm install -g pnpm`);
}
}
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
@ -64,12 +120,12 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
.replace("npm run check-types", "true")
.replace("npm run babel", "true");
.replace(`${packageManager} run check-types`, "true")
.replace(`${packageManager} run babel`, "true");
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
child_process.execSync("npm run build", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);

View File

@ -1,33 +0,0 @@
/*
* 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;
}

View File

@ -1,76 +0,0 @@
/*
* 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);
}

View File

@ -1,25 +0,0 @@
/*
* 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> {}

View File

@ -1,50 +0,0 @@
/*
* 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;
}
}

View File

@ -1,424 +0,0 @@
/*
* 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() {}
}

View File

@ -1,51 +0,0 @@
/*
* 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";
}
}

View File

@ -1,51 +0,0 @@
/*
* 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();
}
}
}

View File

@ -1,91 +0,0 @@
/*
* 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;
}
}

View File

@ -1,157 +0,0 @@
/*
* 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;
}
}
}

View File

@ -1,258 +0,0 @@
/*
* 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);
}
}

View File

@ -1,515 +0,0 @@
/*
* 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;
}
}
}

View File

@ -1,56 +0,0 @@
/*
* 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;
}
}

View File

@ -1,95 +0,0 @@
/*
* 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("_", " ");
}
}
}

View File

@ -1,34 +0,0 @@
/*
* 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;
}
}

View File

@ -1,75 +0,0 @@
/*
* 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();
}
}

View File

@ -1,38 +0,0 @@
/*
* 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];
}
}

View File

@ -1,93 +0,0 @@
/*
* 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;
}
}
}

View File

@ -1,125 +0,0 @@
/*
* 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;
}
}

View File

@ -1,121 +0,0 @@
/*
* 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;
}
}

View File

@ -1,115 +0,0 @@
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();
}
}

View File

@ -1,64 +0,0 @@
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");
}
}

View File

@ -38,7 +38,21 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
const commonResourceFilePaths = [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css"
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
...[
"OpenSans-Light-webfont.woff2",
"OpenSans-Regular-webfont.woff2",
"OpenSans-Bold-webfont.woff2",
"OpenSans-Semibold-webfont.woff2",
"OpenSans-Bold-webfont.woff",
"OpenSans-Light-webfont.woff",
"OpenSans-Regular-webfont.woff",
"OpenSans-Semibold-webfont.woff",
"OpenSans-Regular-webfont.ttf",
"OpenSans-Light-webfont.ttf",
"OpenSans-Semibold-webfont.ttf",
"OpenSans-Bold-webfont.ttf"
].map(path => `node_modules/patternfly/dist/fonts/${path}`)
];
for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
@ -49,7 +63,7 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
}
const resourceFilePaths = ["css/account.css"];
const resourceFilePaths = ["css/account.css", "img/icon-sidebar-active.png", "img/logo.png"];
for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
@ -69,7 +83,7 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"",
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"",
"styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources_common/${path}`)].join(" "),
"styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources-common/${path}`)].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
@ -84,9 +98,4 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"utf8"
)
);
transformCodebase({
"srcDirPath": pathJoin(__dirname, "account-v1-java"),
"destDirPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "java", "org", "keycloak")
});
}

View File

@ -13,6 +13,7 @@ export type BuildOptionsLike = {
cacheDirPath: string;
keycloakifyBuildDirPath: string;
themeNames: string[];
doBuildRetrocompatAccountTheme: boolean;
};
{
@ -36,113 +37,42 @@ export async function generateJavaStackFiles(params: {
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>`,
` 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>${buildOptions.groupId}</groupId>`,
` <artifactId>${buildOptions.artifactId}</artifactId>`,
` <version>${buildOptions.themeVersion}</version>`,
` <name>${buildOptions.artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <java.version>17</java.version>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` <keycloak.version>23.0.0</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>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
` <build>`,
` <plugins>`,
` <plugin>`,
` <groupId>org.apache.maven.plugins</groupId>`,
` <artifactId>maven-shade-plugin</artifactId>`,
` <version>3.5.1</version>`,
` <executions>`,
` <execution>`,
` <phase>package</phase>`,
` <goals>`,
` <goal>shade</goal>`,
` </goals>`,
` </execution>`,
` </executions>`,
` </plugin>`,
` </plugins>`,
` </build>`,
` <dependencies>`,
` <dependency>`,
` <groupId>io.phasetwo.keycloak</groupId>`,
` <artifactId>keycloak-account-v1</artifactId>`,
` <version>0.1</version>`,
` </dependency>`,
` </dependencies>`,
`</project>`
].join("\n");
@ -185,7 +115,7 @@ export async function generateJavaStackFiles(params: {
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
},
...(!implementedThemeTypes.account
...(!implementedThemeTypes.account || !buildOptions.doBuildRetrocompatAccountTheme
? []
: [
{

View File

@ -1,5 +1,5 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
@ -19,8 +19,8 @@ generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_contain
const containerName = "keycloak-testing-container";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: { keycloakVersion: string; buildOptions: BuildOptionsLike }) {
const { keycloakVersion, buildOptions } = params;
export function generateStartKeycloakTestingContainer(params: { jarFilePath: string; keycloakVersion: string; buildOptions: BuildOptionsLike }) {
const { jarFilePath, keycloakVersion, buildOptions } = params;
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
const themeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, themeRelativeDirPath);
@ -30,6 +30,7 @@ export function generateStartKeycloakTestingContainer(params: { keycloakVersion:
Buffer.from(
[
"#!/usr/bin/env bash",
`# If you want to test with Keycloak version prior to 23 use the retrocompat-${pathBasename(jarFilePath)}`,
"",
`docker rm ${containerName} || true`,
"",
@ -40,12 +41,19 @@ export function generateStartKeycloakTestingContainer(params: { keycloakVersion:
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
` -v "${pathJoin(
"$(pwd)",
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
...fs
.readdirSync(themeDirPath)
.filter(name => fs.lstatSync(pathJoin(themeDirPath, name)).isDirectory())
.map(
themeName =>
` -v "${pathJoin(".", themeRelativeDirPath, themeName).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(
/\\/g,
"/"
)}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev --features=declarative-user-profile`,

View File

@ -1,6 +1,6 @@
import { generateTheme } from "./generateTheme";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, dirname as pathDirname, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
@ -63,13 +63,31 @@ export async function main() {
});
if (buildOptions.doCreateJar) {
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakifyBuildDirPath });
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
const jarDirPath = pathDirname(jarFilePath);
const retrocompatJarFilePath = pathJoin(jarDirPath, "retrocompat-" + pathBasename(jarFilePath));
fs.renameSync(pathJoin(jarDirPath, "original-" + pathBasename(jarFilePath)), retrocompatJarFilePath);
fs.writeFileSync(
pathJoin(jarDirPath, "README.md"),
Buffer.from(
[
`- The ${jarFilePath} is to be used in Keycloak 23 and up. `,
`- The ${retrocompatJarFilePath} is to be used in Keycloak 22 and below.`,
` Note that Keycloak 22 is only supported for login and email theme but not for account themes. `
].join("\n"),
"utf8"
)
);
}
const containerKeycloakVersion = "23.0.0";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});

View File

@ -16,7 +16,7 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
);

View File

@ -1,6 +1,7 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
export async function promptKeycloakVersion() {
const { getLatestsSemVersionedTag } = (() => {
@ -26,6 +27,7 @@ export async function promptKeycloakVersion() {
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
lastKeycloakVersionWithAccountV1,
"11.0.3"
];

View File

@ -45,6 +45,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
return null;
}
document.title = i18n.msgStr("loginTitle", kcContext.realm.displayName);
return (
<div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={getClassName("kcHeaderClass")}>

View File

@ -95,4 +95,5 @@ export type ClassKey =
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass"
| "kcFormButtonsWrapperClass";
| "kcFormButtonsWrapperClass"
| "kcInputGroup";

View File

@ -82,6 +82,7 @@ export declare namespace KcContext {
clientId: string;
name?: string;
description?: string;
attributes: Record<string, string>;
};
isAppInitiatedAction: boolean;
messagesPerField: {

View File

@ -234,7 +234,8 @@ export const kcContextCommonMock: KcContext.Common = {
"showTryAnotherWayLink": false
},
"client": {
"clientId": "myApp"
"clientId": "myApp",
"attributes": {}
},
"scripts": [],
"isAppInitiatedAction": false
@ -314,7 +315,8 @@ export const kcContextMocks = [
"actionUri": "#",
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
}
}),
id<KcContext.Error>({
@ -322,7 +324,8 @@ export const kcContextMocks = [
"pageId": "error.ftl",
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
},
"message": {
"type": "error",
@ -496,7 +499,8 @@ export const kcContextMocks = [
},
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
},
"logoutConfirm": { "code": "123", skipLink: false }
}),

View File

@ -67,6 +67,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
// css classes for input
"kcInputLargeClass": "input-lg",
"kcInputGroup": "pf-c-input-group",
// css classes for form accessability
"kcSrOnlyClass": "sr-only",
@ -93,7 +94,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
//css classes for the OTP Login Form
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select",
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select col-xs-12",
"kcSelectOTPListItemClass": "card-pf-body card-pf-top-element",
"kcAuthenticatorOtpCircleClass": "fa fa-mobile card-pf-icon-circle",
"kcSelectOTPItemHeadingClass": "card-pf-title text-center",

View File

@ -37,22 +37,18 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
displayInfo={social.displayInfo}
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
infoNode={
realm.password &&
realm.registrationAllowed &&
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
}
>
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && getClassName("kcContentWrapperClass"))}>

View File

@ -1,12 +1,11 @@
import { clsx } from "keycloakify/tools/clsx";
import Template from "../Template";
import { I18n } from "../i18n";
import { KcContext } from "../kcContext";
import { useGetClassName } from "../lib/useGetClassName";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { PageProps } from "./PageProps";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props;
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url } = kcContext;
const { msg, msgStr } = i18n;

View File

@ -2,11 +2,10 @@ import { clsx } from "keycloakify/tools/clsx";
import { PageProps } from "./PageProps";
import { KcContext } from "../kcContext";
import { I18n } from "../i18n";
import Template from "../Template";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props;
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, oauth, client } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
@ -18,105 +16,77 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { msg, msgStr } = i18n;
useEffect(() => {
let isCleanedUp = false;
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
});
(async () => {
await prLoaded;
if (isCleanedUp) {
return;
}
evaluateInlineScript();
})();
return () => {
isCleanedUp = true;
remove();
};
}, []);
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("doLogIn")}>
<form id="kc-otp-login-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && (
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcInputWrapperClass")}>
{otpLogin.userOtpCredentials.map(otpCredential => (
<div key={otpCredential.id} className={getClassName("kcSelectOTPListClass")}>
<input type="hidden" value="${otpCredential.id}" />
<div className={getClassName("kcSelectOTPListItemClass")}>
<span className={getClassName("kcAuthenticatorOtpCircleClass")} />
<h2 className={getClassName("kcSelectOTPItemHeadingClass")}>{otpCredential.userLabel}</h2>
<>
<style>
{`
input[type="radio"]:checked~label.kcSelectOTPListClass{
border: 2px solid #39a5dc;
}`}
</style>
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("doLogIn")}>
<form id="kc-otp-login-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && (
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcInputWrapperClass")}>
{otpLogin.userOtpCredentials.map((otpCredential, index) => (
<div key={otpCredential.id}>
<input
id={`kc-otp-credential-${index}`}
name="selectedCredentialId"
type="radio"
value={otpCredential.id}
style={{ display: "none" }}
/>
<label
htmlFor={`kc-otp-credential-${index}`}
key={otpCredential.id}
className={getClassName("kcSelectOTPListClass")}
>
<div className={getClassName("kcSelectOTPListItemClass")}>
<span className={getClassName("kcAuthenticatorOtpCircleClass")} />
<h2 className={getClassName("kcSelectOTPItemHeadingClass")}>{otpCredential.userLabel}</h2>
</div>
</label>
</div>
</div>
))}
))}
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="otp" className={getClassName("kcLabelClass")}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input id="otp" name="otp" autoComplete="off" type="text" className={getClassName("kcInputClass")} autoFocus />
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="otp" className={getClassName("kcLabelClass")}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input id="otp" name="otp" autoComplete="off" type="text" className={getClassName("kcInputClass")} autoFocus />
</div>
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")} />
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")} />
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
</form>
</Template>
</form>
</Template>
</>
);
}
declare const $: any;
function evaluateInlineScript() {
$(document).ready(function () {
// Card Single Select
$(".card-pf-view-single-select").click(function (this: any) {
if ($(this).hasClass("active")) {
$(this).removeClass("active");
$(this).children().removeAttr("name");
} else {
$(".card-pf-view-single-select").removeClass("active");
$(".card-pf-view-single-select").children().removeAttr("name");
$(this).addClass("active");
$(this).children().attr("name", "selectedCredentialId");
}
});
var defaultCred = $(".card-pf-view-single-select")[0];
if (defaultCred) {
defaultCred.click();
}
});
}

View File

@ -1,9 +1,9 @@
import type { LazyExoticComponent } from "react";
import type { I18n } from "keycloakify/login/i18n";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
export type PageProps<KcContext, I18nExtended extends I18n> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

3
src/tools/LazyOrNot.ts Normal file
View File

@ -0,0 +1,3 @@
import type { LazyExoticComponent, ComponentType } from "react";
export type LazyOrNot<Component extends ComponentType<any>> = LazyExoticComponent<Component> | Component;

View File

@ -67,9 +67,6 @@ describe("Ensure it's able to extract used Keycloak resources", () => {
`
});
console.log(paths);
console.log(expectedPaths);
expect(same(paths, expectedPaths)).toBe(true);
});

View File

@ -124,7 +124,7 @@ describe("bin/css-transforms", () => {
}
.my-div2 {
background: url(/logo192.png) no-repeat center center;
background: url(/logo192.png) repeat center center;
}
.my-div {
@ -135,11 +135,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = `
.my-div {
background: var(--url1f9ef5a892c104c);
background: var(--urla882a969fd39473) no-repeat center center;
}
.my-div2 {
background: var(--url1f9ef5a892c104c);
background: var(--urla882a969fd39473) repeat center center;
}
.my-div {
@ -150,7 +150,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
"url1f9ef5a892c104c": "url(/logo192.png) no-repeat center center",
"urla882a969fd39473": "url(/logo192.png)",
"urldd75cab58377c19": "url(/static/media/something.svg)"
};
@ -165,7 +165,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = `
:root {
--url1f9ef5a892c104c: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center;
--urla882a969fd39473: url(\${url.resourcesPath}/build/logo192.png);
--urldd75cab58377c19: url(\${url.resourcesPath}/build/static/media/something.svg);
}
`;
@ -191,11 +191,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = `
.my-div {
background: var(--urlf8277cddaa2be78);
background: var(--url749a3139386b2c8) no-repeat center center;
}
.my-div2 {
background: var(--urlf8277cddaa2be78);
background: var(--url749a3139386b2c8) no-repeat center center;
}
.my-div {
@ -206,7 +206,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
"urlf8277cddaa2be78": "url(/x/y/z/logo192.png) no-repeat center center",
"url749a3139386b2c8": "url(/x/y/z/logo192.png)",
"url8bdc0887b97ac9a": "url(/x/y/z/static/media/something.svg)"
};
@ -221,7 +221,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = `
:root {
--urlf8277cddaa2be78: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center;
--url749a3139386b2c8: url(\${url.resourcesPath}/build/logo192.png);
--url8bdc0887b97ac9a: url(\${url.resourcesPath}/build/static/media/something.svg);
}
`;