keycloakify 9.5.7 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/account/Fallback.js +12 -0
  2. package/account/Fallback.js.map +1 -1
  3. package/account/TemplateProps.d.ts +1 -1
  4. package/account/kcContext/KcContext.d.ts +144 -1
  5. package/account/kcContext/KcContext.js.map +1 -1
  6. package/account/kcContext/createGetKcContext.d.ts +1 -0
  7. package/account/kcContext/createGetKcContext.js +7 -1
  8. package/account/kcContext/createGetKcContext.js.map +1 -1
  9. package/account/kcContext/kcContextMocks.js +55 -2
  10. package/account/kcContext/kcContextMocks.js.map +1 -1
  11. package/account/lib/useGetClassName.js +8 -1
  12. package/account/lib/useGetClassName.js.map +1 -1
  13. package/account/pages/Applications.d.ts +7 -0
  14. package/account/pages/Applications.js +24 -0
  15. package/account/pages/Applications.js.map +1 -0
  16. package/account/pages/Log.d.ts +7 -0
  17. package/account/pages/Log.js +13 -0
  18. package/account/pages/Log.js.map +1 -0
  19. package/account/pages/Sessions.d.ts +7 -0
  20. package/account/pages/Sessions.js +18 -0
  21. package/account/pages/Sessions.js.map +1 -0
  22. package/account/pages/Totp.d.ts +7 -0
  23. package/account/pages/Totp.js +20 -0
  24. package/account/pages/Totp.js.map +1 -0
  25. package/bin/keycloakify/generateFtl/pageId.d.ts +1 -1
  26. package/bin/keycloakify/generateFtl/pageId.js +1 -1
  27. package/bin/keycloakify/generateFtl/pageId.js.map +1 -1
  28. package/login/kcContext/createGetKcContext.d.ts +1 -0
  29. package/login/kcContext/createGetKcContext.js +7 -1
  30. package/login/kcContext/createGetKcContext.js.map +1 -1
  31. package/login/kcContext/kcContextMocks.js +119 -1
  32. package/login/kcContext/kcContextMocks.js.map +1 -1
  33. package/package.json +21 -1
  34. package/src/account/Fallback.tsx +12 -0
  35. package/src/account/TemplateProps.ts +14 -1
  36. package/src/account/kcContext/KcContext.ts +148 -1
  37. package/src/account/kcContext/createGetKcContext.ts +9 -1
  38. package/src/account/kcContext/kcContextMocks.ts +79 -1
  39. package/src/account/lib/useGetClassName.ts +8 -1
  40. package/src/account/pages/Applications.tsx +138 -0
  41. package/src/account/pages/Log.tsx +70 -0
  42. package/src/account/pages/Sessions.tsx +68 -0
  43. package/src/account/pages/Totp.tsx +236 -0
  44. package/src/bin/keycloakify/generateFtl/pageId.ts +1 -1
  45. package/src/login/kcContext/createGetKcContext.ts +9 -1
  46. package/src/login/kcContext/kcContextMocks.ts +120 -1
@@ -0,0 +1,138 @@
1
+ import { clsx } from "keycloakify/tools/clsx";
2
+ import type { PageProps } from "keycloakify/account/pages/PageProps";
3
+ import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
4
+ import type { KcContext } from "../kcContext";
5
+ import type { I18n } from "../i18n";
6
+
7
+ function isArrayWithEmptyObject(variable: any): boolean {
8
+ return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
9
+ }
10
+
11
+ export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
12
+ const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
13
+
14
+ const { getClassName } = useGetClassName({
15
+ doUseDefaultCss,
16
+ classes
17
+ });
18
+
19
+ const {
20
+ url,
21
+ applications: { applications },
22
+ stateChecker
23
+ } = kcContext;
24
+
25
+ const { msg, advancedMsg } = i18n;
26
+
27
+ return (
28
+ <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="applications">
29
+ <div className="row">
30
+ <div className="col-md-10">
31
+ <h2>{msg("applicationsHtmlTitle")}</h2>
32
+ </div>
33
+
34
+ <form action={url.applicationsUrl} method="post">
35
+ <input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
36
+ <input type="hidden" id="referrer" name="referrer" value={stateChecker} />
37
+
38
+ <table className="table table-striped table-bordered">
39
+ <thead>
40
+ <tr>
41
+ <td>{msg("application")}</td>
42
+ <td>{msg("availableRoles")}</td>
43
+ <td>{msg("grantedPermissions")}</td>
44
+ <td>{msg("additionalGrants")}</td>
45
+ <td>{msg("action")}</td>
46
+ </tr>
47
+ </thead>
48
+
49
+ <tbody>
50
+ {applications.map(application => (
51
+ <tr key={application.client.clientId}>
52
+ <td>
53
+ {application.effectiveUrl && (
54
+ <a href={application.effectiveUrl}>
55
+ {(application.client.name && advancedMsg(application.client.name)) || application.client.clientId}
56
+ </a>
57
+ )}
58
+ {!application.effectiveUrl &&
59
+ ((application.client.name && advancedMsg(application.client.name)) || application.client.clientId)}
60
+ </td>
61
+
62
+ <td>
63
+ {!isArrayWithEmptyObject(application.realmRolesAvailable) &&
64
+ application.realmRolesAvailable.map(role => (
65
+ <span key={role.name}>
66
+ {role.description ? advancedMsg(role.description) : advancedMsg(role.name)}
67
+ {role !== application.realmRolesAvailable[application.realmRolesAvailable.length - 1] && ", "}
68
+ </span>
69
+ ))}
70
+ {!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}
71
+ {application.resourceRolesAvailable &&
72
+ Object.keys(application.resourceRolesAvailable).map(resource => (
73
+ <span key={resource}>
74
+ {!isArrayWithEmptyObject(application.realmRolesAvailable) && ", "}
75
+ {application.resourceRolesAvailable[resource].map(clientRole => (
76
+ <span key={clientRole.roleName}>
77
+ {clientRole.roleDescription
78
+ ? advancedMsg(clientRole.roleDescription)
79
+ : advancedMsg(clientRole.roleName)}{" "}
80
+ {msg("inResource")}{" "}
81
+ <strong>
82
+ {clientRole.clientName ? advancedMsg(clientRole.clientName) : clientRole.clientId}
83
+ </strong>
84
+ {clientRole !==
85
+ application.resourceRolesAvailable[resource][
86
+ application.resourceRolesAvailable[resource].length - 1
87
+ ] && ", "}
88
+ </span>
89
+ ))}
90
+ </span>
91
+ ))}
92
+ </td>
93
+
94
+ <td>
95
+ {application.client.consentRequired ? (
96
+ application.clientScopesGranted.map(claim => (
97
+ <span key={claim}>
98
+ {advancedMsg(claim)}
99
+ {claim !== application.clientScopesGranted[application.clientScopesGranted.length - 1] && ", "}
100
+ </span>
101
+ ))
102
+ ) : (
103
+ <strong>{msg("fullAccess")}</strong>
104
+ )}
105
+ </td>
106
+
107
+ <td>
108
+ {application.additionalGrants.map(grant => (
109
+ <span key={grant}>
110
+ {advancedMsg(grant)}
111
+ {grant !== application.additionalGrants[application.additionalGrants.length - 1] && ", "}
112
+ </span>
113
+ ))}
114
+ </td>
115
+
116
+ <td>
117
+ {(application.client.consentRequired && application.clientScopesGranted.length > 0) ||
118
+ application.additionalGrants.length > 0 ? (
119
+ <button
120
+ type="submit"
121
+ className={clsx(getClassName("kcButtonPrimaryClass"), getClassName("kcButtonClass"))}
122
+ id={`revoke-${application.client.clientId}`}
123
+ name="clientId"
124
+ value={application.client.id}
125
+ >
126
+ {msg("revoke")}
127
+ </button>
128
+ ) : null}
129
+ </td>
130
+ </tr>
131
+ ))}
132
+ </tbody>
133
+ </table>
134
+ </form>
135
+ </div>
136
+ </Template>
137
+ );
138
+ }
@@ -0,0 +1,70 @@
1
+ import type { PageProps } from "keycloakify/account/pages/PageProps";
2
+ import type { KcContext } from "../kcContext";
3
+ import type { I18n } from "../i18n";
4
+ import { Key } from "react";
5
+ import { useGetClassName } from "../lib/useGetClassName";
6
+
7
+ export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
8
+ const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
9
+
10
+ const { getClassName } = useGetClassName({
11
+ doUseDefaultCss,
12
+ classes
13
+ });
14
+
15
+ const { log } = kcContext;
16
+
17
+ const { msg } = i18n;
18
+
19
+ return (
20
+ <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
21
+ <div className={getClassName("kcContentWrapperClass")}>
22
+ <div className="col-md-10">
23
+ <h2>{msg("accountLogHtmlTitle")}</h2>
24
+ </div>
25
+
26
+ <table className="table table-striped table-bordered">
27
+ <thead>
28
+ <tr>
29
+ <td>{msg("date")}</td>
30
+ <td>{msg("event")}</td>
31
+ <td>{msg("ip")}</td>
32
+ <td>{msg("client")}</td>
33
+ <td>{msg("details")}</td>
34
+ </tr>
35
+ </thead>
36
+
37
+ <tbody>
38
+ {log.events.map(
39
+ (
40
+ event: {
41
+ date: string | number | Date;
42
+ event: string;
43
+ ipAddress: string;
44
+ client: any;
45
+ details: any[];
46
+ },
47
+ index: Key | null | undefined
48
+ ) => (
49
+ <tr key={index}>
50
+ <td>{event.date ? new Date(event.date).toLocaleString() : ""}</td>
51
+ <td>{event.event}</td>
52
+ <td>{event.ipAddress}</td>
53
+ <td>{event.client || ""}</td>
54
+ <td>
55
+ {event.details.map((detail, detailIndex) => (
56
+ <span key={detailIndex}>
57
+ {`${detail.key} = ${detail.value}`}
58
+ {detailIndex < event.details.length - 1 && ", "}
59
+ </span>
60
+ ))}
61
+ </td>
62
+ </tr>
63
+ )
64
+ )}
65
+ </tbody>
66
+ </table>
67
+ </div>
68
+ </Template>
69
+ );
70
+ }
@@ -0,0 +1,68 @@
1
+ import { clsx } from "keycloakify/tools/clsx";
2
+ import type { PageProps } from "keycloakify/account/pages/PageProps";
3
+ import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
4
+ import type { KcContext } from "../kcContext";
5
+ import type { I18n } from "../i18n";
6
+
7
+ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
8
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
9
+
10
+ const { getClassName } = useGetClassName({
11
+ doUseDefaultCss,
12
+ classes
13
+ });
14
+
15
+ console.log({ kcContext });
16
+ const { url, stateChecker, sessions } = kcContext;
17
+
18
+ const { msg } = i18n;
19
+ console.log({ sdf: kcContext.locale?.supported });
20
+ console.log({ asdf: "asdf" });
21
+ return (
22
+ <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
23
+ <div className={getClassName("kcContentWrapperClass")}>
24
+ <div className="col-md-10">
25
+ <h2>{msg("sessionsHtmlTitle")}</h2>
26
+ </div>
27
+ </div>
28
+
29
+ <table className="table table-striped table-bordered">
30
+ <thead>
31
+ <tr>
32
+ <th>{msg("ip")}</th>
33
+ <th>{msg("started")}</th>
34
+ <th>{msg("lastAccess")}</th>
35
+ <th>{msg("expires")}</th>
36
+ <th>{msg("clients")}</th>
37
+ </tr>
38
+ </thead>
39
+
40
+ <tbody role="rowgroup">
41
+ {sessions.sessions.map((session, index: number) => (
42
+ <tr key={index}>
43
+ <td>{session.ipAddress}</td>
44
+ <td>{session?.started}</td>
45
+ <td>{session?.lastAccess}</td>
46
+ <td>{session?.expires}</td>
47
+ <td>
48
+ {session.clients.map((client: string, clientIndex: number) => (
49
+ <div key={clientIndex}>
50
+ {client}
51
+ <br />
52
+ </div>
53
+ ))}
54
+ </td>
55
+ </tr>
56
+ ))}
57
+ </tbody>
58
+ </table>
59
+
60
+ <form action={url.sessionsUrl} method="post">
61
+ <input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
62
+ <button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
63
+ {msg("doLogOutAllSessions")}
64
+ </button>
65
+ </form>
66
+ </Template>
67
+ );
68
+ }
@@ -0,0 +1,236 @@
1
+ import { clsx } from "keycloakify/tools/clsx";
2
+ import type { PageProps } from "keycloakify/account/pages/PageProps";
3
+ import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
4
+ import type { KcContext } from "../kcContext";
5
+ import type { I18n } from "../i18n";
6
+ import { MessageKey } from "keycloakify/account/i18n/i18n";
7
+
8
+ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
9
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
10
+ const { getClassName } = useGetClassName({
11
+ doUseDefaultCss,
12
+ classes
13
+ });
14
+
15
+ const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
16
+
17
+ const { msg, msgStr } = i18n;
18
+
19
+ const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
20
+ "HmacSHA1": "SHA1",
21
+ "HmacSHA256": "SHA256",
22
+ "HmacSHA512": "SHA512"
23
+ };
24
+
25
+ return (
26
+ <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
27
+ <>
28
+ <div className="row">
29
+ <div className="col-md-10">
30
+ <h2>{msg("authenticatorTitle")}</h2>
31
+ </div>
32
+ {totp.otpCredentials.length === 0 && (
33
+ <div className="subtitle col-md-2">
34
+ <span className="required">*</span>
35
+ {msg("requiredFields")}
36
+ </div>
37
+ )}
38
+ </div>
39
+ {totp.enabled && (
40
+ <table className="table table-bordered table-striped">
41
+ <thead>
42
+ {totp.otpCredentials.length > 1 ? (
43
+ <tr>
44
+ <th colSpan={4}>{msg("configureAuthenticators")}</th>
45
+ </tr>
46
+ ) : (
47
+ <tr>
48
+ <th colSpan={3}>{msg("configureAuthenticators")}</th>
49
+ </tr>
50
+ )}
51
+ </thead>
52
+ <tbody>
53
+ {totp.otpCredentials.map((credential, index) => (
54
+ <tr key={index}>
55
+ <td className="provider">{msg("mobile")}</td>
56
+ {totp.otpCredentials.length > 1 && <td className="provider">{credential.id}</td>}
57
+ <td className="provider">{credential.userLabel || ""}</td>
58
+ <td className="action">
59
+ <form action={url.totpUrl} method="post" className="form-inline">
60
+ <input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
61
+ <input type="hidden" id="submitAction" name="submitAction" value="Delete" />
62
+ <input type="hidden" id="credentialId" name="credentialId" value={credential.id} />
63
+ <button id={`remove-mobile-${index}`} className="btn btn-default">
64
+ <i className="pficon pficon-delete"></i>
65
+ </button>
66
+ </form>
67
+ </td>
68
+ </tr>
69
+ ))}
70
+ </tbody>
71
+ </table>
72
+ )}
73
+ {!totp.enabled && (
74
+ <div>
75
+ <hr />
76
+ <ol id="kc-totp-settings">
77
+ <li>
78
+ <p>{msg("totpStep1")}</p>
79
+
80
+ <ul id="kc-totp-supported-apps">
81
+ {totp.supportedApplications.map(app => (
82
+ <li key={app}>{msg(app as MessageKey)}</li>
83
+ ))}
84
+ </ul>
85
+ </li>
86
+
87
+ {mode && mode == "manual" ? (
88
+ <>
89
+ <li>
90
+ <p>{msg("totpManualStep2")}</p>
91
+ <p>
92
+ <span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
93
+ </p>
94
+ <p>
95
+ <a href={totp.qrUrl} id="mode-barcode">
96
+ {msg("totpScanBarcode")}
97
+ </a>
98
+ </p>
99
+ </li>
100
+ <li>
101
+ <p>{msg("totpManualStep3")}</p>
102
+ <p>
103
+ <ul>
104
+ <li id="kc-totp-type">
105
+ {msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
106
+ </li>
107
+ <li id="kc-totp-algorithm">
108
+ {msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
109
+ </li>
110
+ <li id="kc-totp-digits">
111
+ {msg("totpDigits")}: {totp.policy.digits}
112
+ </li>
113
+ {totp.policy.type === "totp" ? (
114
+ <li id="kc-totp-period">
115
+ {msg("totpInterval")}: {totp.policy.period}
116
+ </li>
117
+ ) : (
118
+ <li id="kc-totp-counter">
119
+ {msg("totpCounter")}: {totp.policy.initialCounter}
120
+ </li>
121
+ )}
122
+ </ul>
123
+ </p>
124
+ </li>
125
+ </>
126
+ ) : (
127
+ <li>
128
+ <p>{msg("totpStep2")}</p>
129
+ <p>
130
+ <img
131
+ id="kc-totp-secret-qr-code"
132
+ src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
133
+ alt="Figure: Barcode"
134
+ />
135
+ </p>
136
+ <p>
137
+ <a href={totp.manualUrl} id="mode-manual">
138
+ {msg("totpUnableToScan")}
139
+ </a>
140
+ </p>
141
+ </li>
142
+ )}
143
+ <li>
144
+ <p>{msg("totpStep3")}</p>
145
+ <p>{msg("totpStep3DeviceName")}</p>
146
+ </li>
147
+ </ol>
148
+ <hr />
149
+ <form action={url.totpUrl} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
150
+ <input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
151
+ <div className={getClassName("kcFormGroupClass")}>
152
+ <div className="col-sm-2 col-md-2">
153
+ <label htmlFor="totp" className="control-label">
154
+ {msg("authenticatorCode")}
155
+ </label>
156
+ <span className="required">*</span>
157
+ </div>
158
+ <div className="col-sm-10 col-md-10">
159
+ <input
160
+ type="text"
161
+ id="totp"
162
+ name="totp"
163
+ autoComplete="off"
164
+ className={getClassName("kcInputClass")}
165
+ aria-invalid={messagesPerField.existsError("totp")}
166
+ />
167
+
168
+ {messagesPerField.existsError("totp") && (
169
+ <span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
170
+ {messagesPerField.get("totp")}
171
+ </span>
172
+ )}
173
+ </div>
174
+ <input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
175
+ {mode && <input type="hidden" id="mode" value={mode} />}
176
+ </div>
177
+
178
+ <div className={getClassName("kcFormGroupClass")}>
179
+ <div className="col-sm-2 col-md-2">
180
+ <label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
181
+ {msg("totpDeviceName")}
182
+ </label>
183
+ {totp.otpCredentials.length >= 1 && <span className="required">*</span>}
184
+ </div>
185
+ <div className="col-sm-10 col-md-10">
186
+ <input
187
+ type="text"
188
+ id="userLabel"
189
+ name="userLabel"
190
+ autoComplete="off"
191
+ className={getClassName("kcInputClass")}
192
+ aria-invalid={messagesPerField.existsError("userLabel")}
193
+ />
194
+ {messagesPerField.existsError("userLabel") && (
195
+ <span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
196
+ {messagesPerField.get("userLabel")}
197
+ </span>
198
+ )}
199
+ </div>
200
+ </div>
201
+
202
+ <div id="kc-form-buttons" className={clsx(getClassName("kcFormGroupClass"), "text-right")}>
203
+ <div className={getClassName("kcInputWrapperClass")}>
204
+ <input
205
+ type="submit"
206
+ className={clsx(
207
+ getClassName("kcButtonClass"),
208
+ getClassName("kcButtonPrimaryClass"),
209
+ getClassName("kcButtonLargeClass")
210
+ )}
211
+ id="saveTOTPBtn"
212
+ value={msgStr("doSave")}
213
+ />
214
+ <button
215
+ type="submit"
216
+ className={clsx(
217
+ getClassName("kcButtonClass"),
218
+ getClassName("kcButtonDefaultClass"),
219
+ getClassName("kcButtonLargeClass"),
220
+ getClassName("kcButtonLargeClass")
221
+ )}
222
+ id="cancelTOTPBtn"
223
+ name="submitAction"
224
+ value="Cancel"
225
+ >
226
+ {msg("doCancel")}
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </form>
231
+ </div>
232
+ )}
233
+ </>
234
+ </Template>
235
+ );
236
+ }
@@ -27,7 +27,7 @@ export const loginThemePageIds = [
27
27
  "saml-post-form.ftl"
28
28
  ] as const;
29
29
 
30
- export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
30
+ export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const;
31
31
 
32
32
  export type LoginThemePageId = (typeof loginThemePageIds)[number];
33
33
  export type AccountThemePageId = (typeof accountThemePageIds)[number];
@@ -12,8 +12,9 @@ import { symToStr } from "tsafe/symToStr";
12
12
 
13
13
  export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
14
14
  mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
15
+ mockProperties?: Record<string, string>;
15
16
  }) {
16
- const { mockData } = params ?? {};
17
+ const { mockData, mockProperties } = params ?? {};
17
18
 
18
19
  function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
19
20
  mockPageId?: PageId;
@@ -141,6 +142,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
141
142
  }
142
143
  }
143
144
 
145
+ if (mockProperties !== undefined) {
146
+ deepAssign({
147
+ "target": kcContext.properties,
148
+ "source": mockProperties
149
+ });
150
+ }
151
+
144
152
  return { kcContext };
145
153
  }
146
154