Compare commits
96 Commits
v9.0.0-rc.
...
v9.1.8
Author | SHA1 | Date | |
---|---|---|---|
0a74a95283 | |||
74ef2c3dff | |||
9976dfacc0 | |||
659f8ddc7a | |||
9e4cc2ae57 | |||
a27d78fcdf | |||
e507435bcb | |||
d5f234909f | |||
c17f721625 | |||
600705130f | |||
5c5dce1422 | |||
53585bf2f0 | |||
116f88a503 | |||
aaba8cd2c7 | |||
b67aeb0d3a | |||
f620562d68 | |||
5231d0eaa1 | |||
cb470e3573 | |||
0a0f90aa2e | |||
635207d12c | |||
5e4a829413 | |||
b13b3fd92e | |||
564dc8e6f1 | |||
6e4cced8c6 | |||
29a4a5027c | |||
ee327448b4 | |||
d078960c5c | |||
2e8cd375fc | |||
1f6751cb01 | |||
3cca4e31cd | |||
b93902800c | |||
70f6bb3fda | |||
c075cb6311 | |||
d7db85b062 | |||
b442e7d958 | |||
a495ae637f | |||
94748a96a9 | |||
7657429054 | |||
2ff6dbf975 | |||
4f34628c14 | |||
6ff2111cee | |||
85957980f6 | |||
a6dcfe2c87 | |||
c32d590fbb | |||
ab41462f71 | |||
951f16b1a5 | |||
b5818888bb | |||
1a326bf7e4 | |||
e1afc1cf7a | |||
bb007ddce5 | |||
b5dd0317c7 | |||
3c54541a73 | |||
2657f01135 | |||
7223409eb1 | |||
c41eae63e7 | |||
c8b85c43aa | |||
e918788c3f | |||
b53f4f997c | |||
481d93ebc4 | |||
1ce666f136 | |||
49a8e702bc | |||
5d59e652d7 | |||
02af8c7311 | |||
fadf4e867c | |||
0839859fef | |||
c122b48e35 | |||
cebb297bbf | |||
2e31b796f7 | |||
e0a61b51cb | |||
46e50e622b | |||
7cfa1df0b2 | |||
8a63648339 | |||
bb6b026720 | |||
2a13b314dc | |||
4506b3f6d4 | |||
804abef0de | |||
7e932b920e | |||
46fdfbc507 | |||
a4ff8607c5 | |||
7fe4eeda57 | |||
9f25cddaa4 | |||
eb64fe60d0 | |||
36f404e17d | |||
5398590939 | |||
96d5cfea14 | |||
79007ebd55 | |||
fcb519dac3 | |||
2b487aa959 | |||
733feadcb2 | |||
5ae568f19c | |||
0e51807856 | |||
b6eb165207 | |||
d26dbf4b3d | |||
e06ef01f72 | |||
a722582709 | |||
de64deb5c5 |
@ -158,6 +158,70 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zavoloklom",
|
||||
"name": "Sergey Kupletsky",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4151869?v=4",
|
||||
"profile": "https://github.com/zavoloklom",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rome-user",
|
||||
"name": "rome-user",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/114131048?v=4",
|
||||
"profile": "https://github.com/rome-user",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "celinepelletier",
|
||||
"name": "Céline Pelletier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82821620?v=4",
|
||||
"profile": "https://github.com/celinepelletier",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
@ -165,5 +229,6 @@
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "keycloakify",
|
||||
"projectOwner": "keycloakify"
|
||||
"projectOwner": "keycloakify",
|
||||
"commitType": "docs"
|
||||
}
|
||||
|
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@ -3,6 +3,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v5
|
||||
- v6
|
||||
- v7
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
32
README.md
32
README.md
@ -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 greenlighted
|
||||
> by the Keycloak team. Resolution is only a matter of time.
|
||||
|
||||
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
|
||||
You can update your Keycloak, your Keycloakify generated theme won't break.
|
||||
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 👼
|
||||
|
||||
@ -116,6 +104,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -129,8 +126,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
|
||||
## v9.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
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.1.8",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -22,7 +22,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
],
|
||||
"htmlClassName": undefined,
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
});
|
||||
|
||||
|
@ -11,4 +11,4 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||
|
@ -7,6 +7,7 @@ export type KcContext = KcContext.Password | KcContext.Account;
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
themeVersion: string;
|
||||
keycloakifyVersion: string;
|
||||
themeType: "account";
|
||||
themeName: string;
|
||||
|
@ -9,6 +9,7 @@ const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUB
|
||||
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "account",
|
||||
"themeName": "my-theme-name",
|
||||
|
@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcHtmlClass": undefined,
|
||||
"kcBodyClass": undefined,
|
||||
"kcButtonClass": "btn",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
|
@ -51,7 +51,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
id="username"
|
||||
name="username"
|
||||
disabled={!realm.editUsernameAllowed}
|
||||
value={account.username ?? ""}
|
||||
defaultValue={account.username ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,7 +66,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
|
||||
<input type="text" className="form-control" id="email" name="email" autoFocus defaultValue={account.email ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +79,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
|
||||
<input type="text" className="form-control" id="firstName" name="firstName" defaultValue={account.firstName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -92,7 +92,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
|
||||
<input type="text" className="form-control" id="lastName" name="lastName" defaultValue={account.lastName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -4,7 +4,7 @@ import { crawl } from "./tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { themeTypes } from "./constants";
|
||||
|
||||
const themeSrcDirBasename = "keycloak-theme";
|
||||
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
|
||||
|
||||
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
|
||||
export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
@ -14,13 +14,13 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
|
||||
.map(fileRelativePath => {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
|
||||
if (split.length !== 2) {
|
||||
return undefined;
|
||||
for (const themeSrcDirBasename of themeSrcDirBasenames) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
}
|
||||
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
return undefined;
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
@ -38,7 +38,7 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
console.error(
|
||||
[
|
||||
"Can't locate your theme source directory. It should be either: ",
|
||||
"src/ or src/keycloak-theme.",
|
||||
"src/ or src/keycloak-theme or src/keycloak_theme.",
|
||||
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
|
||||
].join("\n")
|
||||
);
|
||||
|
@ -478,6 +478,12 @@
|
||||
"error.ftl" == pageId &&
|
||||
are_same_path(path, ["realm"]) &&
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
"masterAdminClient" == key
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
|
@ -121,6 +121,27 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
// Remove part of the document marked as ignored.
|
||||
{
|
||||
const startTags = $('meta[name="keycloakify-ignore-start"]');
|
||||
|
||||
startTags.each((...[, startTag]) => {
|
||||
const $startTag = $(startTag);
|
||||
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
|
||||
|
||||
if ($endTag.length) {
|
||||
let currentNode = $startTag.next();
|
||||
while (currentNode.length && !currentNode.is($endTag)) {
|
||||
currentNode.remove();
|
||||
currentNode = $startTag.next();
|
||||
}
|
||||
|
||||
$startTag.remove();
|
||||
$endTag.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(params: { pageId: string }): {
|
||||
|
@ -10,6 +10,8 @@ export const loginThemePageIds = [
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-oauth2-device-verify-user-code.ftl",
|
||||
"login-oauth-grant.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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> {}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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("_", " ");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
|
||||
pathJoin(accountV1DirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
"accountResourceProvider=org.keycloak.services.resources.account.AccountFormService",
|
||||
"accountResourceProvider=account-v1",
|
||||
"",
|
||||
"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",
|
||||
"",
|
||||
@ -84,9 +84,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")
|
||||
});
|
||||
}
|
||||
|
@ -36,113 +36,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>999.0.0-SNAPSHOT</keycloak.version>`,
|
||||
` <guava.version>32.0.0-jre</guava.version>`,
|
||||
` <lombok.version>1.18.28</lombok.version>`,
|
||||
` <auto-service.version>1.1.1</auto-service.version>`,
|
||||
` </properties>`,
|
||||
` <build>`,
|
||||
` <plugins>`,
|
||||
` <plugin>`,
|
||||
` <artifactId>maven-compiler-plugin</artifactId>`,
|
||||
` <version>3.11.0</version>`,
|
||||
` <configuration>`,
|
||||
` <source>\${java.version}</source>`,
|
||||
` <target>\${java.version}</target>`,
|
||||
` <compilerArgument>-Xlint:unchecked</compilerArgument>`,
|
||||
` <compilerArgument>-Xlint:deprecation</compilerArgument>`,
|
||||
` <useIncrementalCompilation>false</useIncrementalCompilation>`,
|
||||
` <annotationProcessorPaths>`,
|
||||
` <path>`,
|
||||
` <groupId>com.google.auto.service</groupId>`,
|
||||
` <artifactId>auto-service</artifactId>`,
|
||||
` <version>\${auto-service.version}</version>`,
|
||||
` </path>`,
|
||||
` <path>`,
|
||||
` <groupId>org.projectlombok</groupId>`,
|
||||
` <artifactId>lombok</artifactId>`,
|
||||
` <version>\${lombok.version}</version>`,
|
||||
` </path>`,
|
||||
` </annotationProcessorPaths>`,
|
||||
` </configuration>`,
|
||||
` </plugin>`,
|
||||
` <plugin>`,
|
||||
` <groupId>org.apache.maven.plugins</groupId>`,
|
||||
` <artifactId>maven-jar-plugin</artifactId>`,
|
||||
` <version>3.2.0</version>`,
|
||||
` <configuration>`,
|
||||
` <archive>`,
|
||||
` <manifestEntries>`,
|
||||
` <Dependencies>`,
|
||||
` <![CDATA[org.keycloak.keycloak-common,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,com.google.guava]]>`,
|
||||
` </Dependencies>`,
|
||||
` </manifestEntries>`,
|
||||
` </archive>`,
|
||||
` </configuration>`,
|
||||
` </plugin>`,
|
||||
` <plugin>`,
|
||||
` <groupId>com.spotify.fmt</groupId>`,
|
||||
` <artifactId>fmt-maven-plugin</artifactId>`,
|
||||
` <version>2.20</version>`,
|
||||
` </plugin>`,
|
||||
` </plugins>`,
|
||||
` </build>`,
|
||||
` <dependencies>`,
|
||||
` <dependency>`,
|
||||
` <groupId>org.projectlombok</groupId>`,
|
||||
` <artifactId>lombok</artifactId>`,
|
||||
` <version>\${lombok.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>com.google.auto.service</groupId>`,
|
||||
` <artifactId>auto-service</artifactId>`,
|
||||
` <version>\${auto-service.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>org.keycloak</groupId>`,
|
||||
` <artifactId>keycloak-server-spi</artifactId>`,
|
||||
` <version>\${keycloak.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>org.keycloak</groupId>`,
|
||||
` <artifactId>keycloak-server-spi-private</artifactId>`,
|
||||
` <version>\${keycloak.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>org.keycloak</groupId>`,
|
||||
` <artifactId>keycloak-services</artifactId>`,
|
||||
` <version>\${keycloak.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>jakarta.ws.rs</groupId>`,
|
||||
` <artifactId>jakarta.ws.rs-api</artifactId>`,
|
||||
` <version>3.1.0</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` <dependency>`,
|
||||
` <groupId>com.google.guava</groupId>`,
|
||||
` <artifactId>guava</artifactId>`,
|
||||
` <version>\${guava.version}</version>`,
|
||||
` <scope>provided</scope>`,
|
||||
` </dependency>`,
|
||||
` </dependencies>`,
|
||||
` <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");
|
||||
|
||||
|
@ -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`,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
||||
import { resources_common, type ThemeType } from "../../constants";
|
||||
import { BuildOptions } from "../BuildOptions";
|
||||
@ -21,12 +21,36 @@ export async function downloadKeycloakStaticResources(
|
||||
keycloakVersion: string;
|
||||
usedResources: {
|
||||
resourcesCommonFilePaths: string[];
|
||||
resourcesFilePaths: string[];
|
||||
} | undefined;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}
|
||||
) {
|
||||
const { themeType, themeDirPath, keycloakVersion, usedResources, buildOptions } = params;
|
||||
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
|
||||
|
||||
// NOTE: Hack for 427
|
||||
const usedResources = (() => {
|
||||
const { usedResources } = params;
|
||||
|
||||
if (usedResources === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
assert(usedResources !== undefined);
|
||||
|
||||
return {
|
||||
"resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
|
||||
{
|
||||
const splitArg = "/dist/";
|
||||
|
||||
if (filePath.includes(splitArg)) {
|
||||
return filePath.split(splitArg)[0] + splitArg;
|
||||
}
|
||||
}
|
||||
|
||||
return pathDirname(filePath);
|
||||
})
|
||||
};
|
||||
})();
|
||||
|
||||
const tmpDirPath = pathJoin(
|
||||
themeDirPath,
|
||||
@ -43,17 +67,7 @@ export async function downloadKeycloakStaticResources(
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||
"destDirPath": resourcesPath,
|
||||
"transformSourceCode":
|
||||
usedResources === undefined
|
||||
? undefined
|
||||
: ({ fileRelativePath, sourceCode }) => {
|
||||
if (!usedResources.resourcesFilePaths.includes(fileRelativePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
"destDirPath": resourcesPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
@ -63,7 +77,7 @@ export async function downloadKeycloakStaticResources(
|
||||
usedResources === undefined
|
||||
? undefined
|
||||
: ({ fileRelativePath, sourceCode }) => {
|
||||
if (!usedResources.resourcesCommonFilePaths.includes(fileRelativePath)) {
|
||||
if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
import * as fs from "fs";
|
||||
import type { ThemeType } from "../../constants";
|
||||
|
||||
/** Assumes the theme type exists */
|
||||
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
|
||||
resourcesCommonFilePaths: string[];
|
||||
resourcesFilePaths: string[];
|
||||
} {
|
||||
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
|
||||
|
||||
const resourcesCommonFilePaths = new Set<string>();
|
||||
const resourcesFilePaths = new Set<string>();
|
||||
|
||||
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
@ -26,58 +24,53 @@ export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string
|
||||
const wrap = readPaths({ rawSourceFile });
|
||||
|
||||
wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
|
||||
wrap.resourcesFilePaths.forEach(filePath => resourcesFilePaths.add(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths),
|
||||
"resourcesFilePaths": Array.from(resourcesFilePaths)
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths)
|
||||
};
|
||||
}
|
||||
|
||||
/** Exported for testing purpose */
|
||||
export function readPaths(params: { rawSourceFile: string }): {
|
||||
resourcesCommonFilePaths: string[];
|
||||
resourcesFilePaths: string[];
|
||||
} {
|
||||
const { rawSourceFile } = params;
|
||||
|
||||
const resourcesCommonFilePaths = new Set<string>();
|
||||
const resourcesFilePaths = new Set<string>();
|
||||
|
||||
for (const isCommon of [true, false]) {
|
||||
const set = isCommon ? resourcesCommonFilePaths : resourcesFilePaths;
|
||||
{
|
||||
const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g");
|
||||
|
||||
{
|
||||
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*}([^\`]+)\``, "g");
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
set.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
|
||||
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
set.add(filePath);
|
||||
}
|
||||
resourcesCommonFilePaths.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const removePrefixSlash = (filePath: string) => (filePath.startsWith("/") ? filePath.slice(1) : filePath);
|
||||
{
|
||||
const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
|
||||
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
resourcesCommonFilePaths.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizePath = (filePath: string) => {
|
||||
filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
filePath = filePath.replace(/\//g, pathSep);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
return {
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(removePrefixSlash),
|
||||
"resourcesFilePaths": Array.from(resourcesFilePaths).map(removePrefixSlash)
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath)
|
||||
};
|
||||
}
|
||||
|
@ -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,14 +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 21 and below.`,
|
||||
` Note that Keycloak 22 is only supported for login and email theme but not for account themes. `
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// We want, however, to test in a container running the latest Keycloak version
|
||||
const containerKeycloakVersion = "21.1.2";
|
||||
const containerKeycloakVersion = "23.0.0";
|
||||
|
||||
generateStartKeycloakTestingContainer({
|
||||
"keycloakVersion": containerKeycloakVersion,
|
||||
jarFilePath,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
|
@ -16,18 +16,25 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): {
|
||||
const { jsCode } = params;
|
||||
|
||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||
(...[, n, u, e]) => `
|
||||
${n}[(function(){
|
||||
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
|
||||
(...[, n, u, matchedFunction, eForFunction]) => {
|
||||
const isArrowFunction = matchedFunction.includes("=>");
|
||||
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
|
||||
|
||||
return `
|
||||
${n}[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||
set: function (){}
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "${u}";
|
||||
})()] = function(${e}) { return "${true ? "/build/" : ""}static/${language}/"`
|
||||
return "${u}";
|
||||
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
];
|
||||
|
||||
const fixedJsCode = jsCode
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
@ -23,24 +23,21 @@ export function usePrepareTemplate(params: {
|
||||
const removeArray: (() => void)[] = [];
|
||||
|
||||
(async () => {
|
||||
const prLoadedArray: Promise<void>[] = [];
|
||||
|
||||
styles.reverse().forEach(href => {
|
||||
for (const style of [...styles].reverse()) {
|
||||
const { prLoaded, remove } = headInsert({
|
||||
"type": "css",
|
||||
"position": "prepend",
|
||||
href
|
||||
"href": style
|
||||
});
|
||||
|
||||
removeArray.push(remove);
|
||||
|
||||
prLoadedArray.push(prLoaded);
|
||||
});
|
||||
// TODO: Find a way to do that in parallel (without breaking the order)
|
||||
await prLoaded;
|
||||
|
||||
await Promise.all(prLoadedArray);
|
||||
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setReady();
|
||||
|
@ -12,6 +12,8 @@ const Error = lazy(() => import("keycloakify/login/pages/Error"));
|
||||
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
|
||||
const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail"));
|
||||
const Terms = lazy(() => import("keycloakify/login/pages/Terms"));
|
||||
const LoginDeviceVerifyUserCode = lazy(() => import("keycloakify/login/pages/LoginDeviceVerifyUserCode"));
|
||||
const LoginOauthGrant = lazy(() => import("keycloakify/login/pages/LoginOauthGrant"));
|
||||
const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
|
||||
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
|
||||
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
|
||||
@ -52,6 +54,10 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||
return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
|
||||
case "terms.ftl":
|
||||
return <Terms kcContext={kcContext} {...rest} />;
|
||||
case "login-oauth2-device-verify-user-code.ftl":
|
||||
return <LoginDeviceVerifyUserCode kcContext={kcContext} {...rest} />;
|
||||
case "login-oauth-grant.ftl":
|
||||
return <LoginOauthGrant kcContext={kcContext} {...rest} />;
|
||||
case "login-otp.ftl":
|
||||
return <LoginOtp kcContext={kcContext} {...rest} />;
|
||||
case "login-username.ftl":
|
||||
|
@ -38,13 +38,15 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": undefined
|
||||
"bodyClassName": getClassName("kcBodyClass")
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
document.title = i18n.msgStr("loginTitle", kcContext.realm.displayName);
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLoginClass")}>
|
||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
||||
|
@ -21,6 +21,7 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
||||
};
|
||||
|
||||
export type ClassKey =
|
||||
| "kcBodyClass"
|
||||
| "kcHtmlClass"
|
||||
| "kcLoginClass"
|
||||
| "kcHeaderClass"
|
||||
@ -93,4 +94,5 @@ export type ClassKey =
|
||||
| "kcSelectOTPListItemClass"
|
||||
| "kcAuthenticatorOtpCircleClass"
|
||||
| "kcSelectOTPItemHeadingClass"
|
||||
| "kcFormOptionsWrapperClass";
|
||||
| "kcFormOptionsWrapperClass"
|
||||
| "kcFormButtonsWrapperClass";
|
||||
|
@ -19,6 +19,8 @@ export type KcContext =
|
||||
| KcContext.LoginResetPassword
|
||||
| KcContext.LoginVerifyEmail
|
||||
| KcContext.Terms
|
||||
| KcContext.LoginDeviceVerifyUserCode
|
||||
| KcContext.LoginOauthGrant
|
||||
| KcContext.LoginOtp
|
||||
| KcContext.LoginUsername
|
||||
| KcContext.WebauthnAuthenticate
|
||||
@ -38,6 +40,7 @@ export type KcContext =
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
themeVersion: string;
|
||||
keycloakifyVersion: string;
|
||||
themeType: "login";
|
||||
themeName: string;
|
||||
@ -79,6 +82,7 @@ export declare namespace KcContext {
|
||||
clientId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
isAppInitiatedAction: boolean;
|
||||
messagesPerField: {
|
||||
@ -242,6 +246,27 @@ export declare namespace KcContext {
|
||||
pageId: "terms.ftl";
|
||||
};
|
||||
|
||||
export type LoginDeviceVerifyUserCode = Common & {
|
||||
pageId: "login-oauth2-device-verify-user-code.ftl";
|
||||
url: {
|
||||
oauth2DeviceVerificationAction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginOauthGrant = Common & {
|
||||
pageId: "login-oauth-grant.ftl";
|
||||
oauth: {
|
||||
code: string;
|
||||
client: string;
|
||||
clientScopesRequested: {
|
||||
consentScreenText: string;
|
||||
}[];
|
||||
};
|
||||
url: {
|
||||
oauthAction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginOtp = Common & {
|
||||
pageId: "login-otp.ftl";
|
||||
otpLogin: {
|
||||
|
@ -105,6 +105,7 @@ const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUB
|
||||
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "login",
|
||||
"themeName": "my-theme-name",
|
||||
@ -233,7 +234,8 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
"showTryAnotherWayLink": false
|
||||
},
|
||||
"client": {
|
||||
"clientId": "myApp"
|
||||
"clientId": "myApp",
|
||||
"attributes": {}
|
||||
},
|
||||
"scripts": [],
|
||||
"isAppInitiatedAction": false
|
||||
@ -242,7 +244,9 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
const loginUrl = {
|
||||
...kcContextCommonMock.url,
|
||||
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"oauth2DeviceVerificationAction": "/auth/realms/myrealm/device",
|
||||
"oauthAction": "/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
|
||||
};
|
||||
|
||||
export const kcContextMocks = [
|
||||
@ -311,7 +315,8 @@ export const kcContextMocks = [
|
||||
"actionUri": "#",
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"baseUrl": "#"
|
||||
"baseUrl": "#",
|
||||
"attributes": {}
|
||||
}
|
||||
}),
|
||||
id<KcContext.Error>({
|
||||
@ -319,7 +324,8 @@ export const kcContextMocks = [
|
||||
"pageId": "error.ftl",
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"baseUrl": "#"
|
||||
"baseUrl": "#",
|
||||
"attributes": {}
|
||||
},
|
||||
"message": {
|
||||
"type": "error",
|
||||
@ -346,6 +352,25 @@ export const kcContextMocks = [
|
||||
...kcContextCommonMock,
|
||||
"pageId": "terms.ftl"
|
||||
}),
|
||||
id<KcContext.LoginDeviceVerifyUserCode>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-oauth2-device-verify-user-code.ftl",
|
||||
url: loginUrl
|
||||
}),
|
||||
id<KcContext.LoginOauthGrant>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-oauth-grant.ftl",
|
||||
oauth: {
|
||||
code: "5-1N4CIzfi1aprIQjmylI-9e3spLCWW9i5d-GDcs-Sw",
|
||||
clientScopesRequested: [
|
||||
{ consentScreenText: "${profileScopeConsentText}" },
|
||||
{ consentScreenText: "${rolesScopeConsentText}" },
|
||||
{ consentScreenText: "${emailScopeConsentText}" }
|
||||
],
|
||||
client: "account"
|
||||
},
|
||||
url: loginUrl
|
||||
}),
|
||||
id<KcContext.LoginOtp>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-otp.ftl",
|
||||
@ -474,7 +499,8 @@ export const kcContextMocks = [
|
||||
},
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"baseUrl": "#"
|
||||
"baseUrl": "#",
|
||||
"attributes": {}
|
||||
},
|
||||
"logoutConfirm": { "code": "123", skipLink: false }
|
||||
}),
|
||||
|
@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcBodyClass": undefined,
|
||||
"kcHtmlClass": "login-pf",
|
||||
"kcLoginClass": "login-pf-page",
|
||||
"kcContentWrapperClass": "row",
|
||||
@ -44,6 +45,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"kcInputClass": "form-control",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormButtonsWrapperClass": undefined,
|
||||
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormSettingClass": "login-pf-settings",
|
||||
|
@ -8,7 +8,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
||||
|
||||
const { msgStr, msg } = i18n;
|
||||
|
||||
assert(kcContext.message !== undefined);
|
||||
assert(
|
||||
kcContext.message !== undefined,
|
||||
"No message in kcContext.message, there will always be a message in production context, add it in your mock"
|
||||
);
|
||||
|
||||
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.supportedApplications.map(app => (
|
||||
<li>{msgStr(app as MessageKey, app)}</li>
|
||||
<li>{msg(app as MessageKey)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
67
src/login/pages/LoginDeviceVerifyUserCode.tsx
Normal file
67
src/login/pages/LoginDeviceVerifyUserCode.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { I18n } from "../i18n";
|
||||
import { KcContext } from "../kcContext";
|
||||
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, Template } = props;
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("oauth2DeviceVerificationTitle")}>
|
||||
<form
|
||||
id="kc-user-verify-device-user-code-form"
|
||||
className={getClassName("kcFormClass")}
|
||||
action={url.oauth2DeviceVerificationAction}
|
||||
method="post"
|
||||
>
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="device-user-code" className={getClassName("kcLabelClass")}>
|
||||
{msg("verifyOAuth2DeviceUserCode")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
id="device-user-code"
|
||||
name="device_user_code"
|
||||
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>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
72
src/login/pages/LoginOauthGrant.tsx
Normal file
72
src/login/pages/LoginOauthGrant.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { PageProps } from "./PageProps";
|
||||
import { KcContext } from "../kcContext";
|
||||
import { I18n } from "../i18n";
|
||||
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, Template } = props;
|
||||
const { url, oauth, client } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={msg("oauthGrantTitle", client.name ? advancedMsgStr(client.name) : client.clientId)}
|
||||
>
|
||||
<div id="kc-oauth" className="content-area">
|
||||
<h3>{msg("oauthGrantRequest")}</h3>
|
||||
<ul>
|
||||
{oauth.clientScopesRequested.map(clientScope => (
|
||||
<li key={clientScope.consentScreenText}>
|
||||
<span>{advancedMsg(clientScope.consentScreenText)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<form className="form-actions" action={url.oauthAction} method="POST">
|
||||
<input type="hidden" name="code" value={oauth.code} />
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options">
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons">
|
||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
value={msgStr("doYes")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonDefaultClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="cancel"
|
||||
id="kc-cancel"
|
||||
type="submit"
|
||||
value={msgStr("doNo")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="clearfix"></div>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -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
3
src/tools/LazyOrNot.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { LazyExoticComponent, ComponentType } from "react";
|
||||
|
||||
export type LazyOrNot<Component extends ComponentType<any>> = LazyExoticComponent<Component> | Component;
|
@ -21,4 +21,13 @@ const meta: ComponentMeta<any> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "This is the server message",
|
||||
type: "info"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
24
stories/login/pages/LoginDeviceVerifyUserCode.stories.tsx
Normal file
24
stories/login/pages/LoginDeviceVerifyUserCode.stories.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "login-oauth2-device-verify-user-code.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
24
stories/login/pages/LoginOauthGrant.stories.tsx
Normal file
24
stories/login/pages/LoginOauthGrant.stories.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "login-oauth-grant.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
@ -9,8 +9,7 @@ describe("Ensure it's able to extract used Keycloak resources", () => {
|
||||
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
"lib/zocial/zocial.css",
|
||||
"node_modules/jquery/dist/jquery.min.js"
|
||||
],
|
||||
"resourcesFilePaths": ["css/login.css"]
|
||||
]
|
||||
};
|
||||
|
||||
it("works with coding style n°1", () => {
|
||||
@ -68,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);
|
||||
});
|
||||
|
||||
|
@ -32,6 +32,10 @@ describe("bin/js-transforms", () => {
|
||||
908:"67c9ed2c"
|
||||
}[e]+".chunk.css"
|
||||
}
|
||||
|
||||
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
||||
|
||||
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
||||
`;
|
||||
it("transforms standalone code properly", () => {
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
@ -52,11 +56,11 @@ describe("bin/js-transforms", () => {
|
||||
}
|
||||
|
||||
__webpack_require__[(function (){
|
||||
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||
var pd = Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(__webpack_require__, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function (){}
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "u";
|
||||
@ -69,11 +73,11 @@ describe("bin/js-transforms", () => {
|
||||
}
|
||||
|
||||
t[(function (){
|
||||
var pd= Object.getOwnPropertyDescriptor(t, "p");
|
||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(t, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function (){}
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "miniCssF";
|
||||
@ -83,7 +87,28 @@ describe("bin/js-transforms", () => {
|
||||
908:"67c9ed2c"
|
||||
} [e] + ".chunk.css"
|
||||
}
|
||||
|
||||
|
||||
n[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(n, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(n, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "u";
|
||||
})()] = e => "/build/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
||||
|
||||
t[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(t, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "miniCssF";
|
||||
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
||||
`;
|
||||
|
||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||
@ -99,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 {
|
||||
@ -110,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 {
|
||||
@ -125,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)"
|
||||
};
|
||||
|
||||
@ -140,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);
|
||||
}
|
||||
`;
|
||||
@ -166,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 {
|
||||
@ -181,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)"
|
||||
};
|
||||
|
||||
@ -196,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);
|
||||
}
|
||||
`;
|
||||
|
Reference in New Issue
Block a user