keycloakify 9.5.8 → 9.6.1

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 (39) hide show
  1. package/README.md +2 -2
  2. package/account/Fallback.js +12 -0
  3. package/account/Fallback.js.map +1 -1
  4. package/account/TemplateProps.d.ts +1 -1
  5. package/account/kcContext/KcContext.d.ts +144 -1
  6. package/account/kcContext/KcContext.js.map +1 -1
  7. package/account/kcContext/kcContextMocks.js +45 -1
  8. package/account/kcContext/kcContextMocks.js.map +1 -1
  9. package/account/lib/useGetClassName.js +8 -1
  10. package/account/lib/useGetClassName.js.map +1 -1
  11. package/account/pages/Applications.d.ts +7 -0
  12. package/account/pages/Applications.js +24 -0
  13. package/account/pages/Applications.js.map +1 -0
  14. package/account/pages/Log.d.ts +7 -0
  15. package/account/pages/Log.js +13 -0
  16. package/account/pages/Log.js.map +1 -0
  17. package/account/pages/Sessions.d.ts +7 -0
  18. package/account/pages/Sessions.js +18 -0
  19. package/account/pages/Sessions.js.map +1 -0
  20. package/account/pages/Totp.d.ts +7 -0
  21. package/account/pages/Totp.js +20 -0
  22. package/account/pages/Totp.js.map +1 -0
  23. package/bin/keycloakify/generateFtl/pageId.d.ts +1 -1
  24. package/bin/keycloakify/generateFtl/pageId.js +1 -1
  25. package/bin/keycloakify/generateFtl/pageId.js.map +1 -1
  26. package/bin/keycloakify/keycloakify.js +9 -4
  27. package/bin/keycloakify/keycloakify.js.map +1 -1
  28. package/package.json +21 -1
  29. package/src/account/Fallback.tsx +12 -0
  30. package/src/account/TemplateProps.ts +14 -1
  31. package/src/account/kcContext/KcContext.ts +148 -1
  32. package/src/account/kcContext/kcContextMocks.ts +68 -0
  33. package/src/account/lib/useGetClassName.ts +8 -1
  34. package/src/account/pages/Applications.tsx +138 -0
  35. package/src/account/pages/Log.tsx +70 -0
  36. package/src/account/pages/Sessions.tsx +68 -0
  37. package/src/account/pages/Totp.tsx +236 -0
  38. package/src/bin/keycloakify/generateFtl/pageId.ts +1 -1
  39. package/src/bin/keycloakify/keycloakify.ts +12 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloakify",
3
- "version": "9.5.8",
3
+ "version": "9.6.1",
4
4
  "description": "Create Keycloak themes using React",
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,8 +70,12 @@
70
70
  "src/account/kcContext/kcContextMocks.ts",
71
71
  "src/account/lib/useGetClassName.ts",
72
72
  "src/account/pages/Account.tsx",
73
+ "src/account/pages/Applications.tsx",
74
+ "src/account/pages/Log.tsx",
73
75
  "src/account/pages/PageProps.ts",
74
76
  "src/account/pages/Password.tsx",
77
+ "src/account/pages/Sessions.tsx",
78
+ "src/account/pages/Totp.tsx",
75
79
  "src/bin/constants.ts",
76
80
  "src/bin/copy-keycloak-resources-to-public.ts",
77
81
  "src/bin/download-builtin-keycloak-theme.ts",
@@ -356,12 +360,24 @@
356
360
  "account/pages/Account.d.ts",
357
361
  "account/pages/Account.js",
358
362
  "account/pages/Account.js.map",
363
+ "account/pages/Applications.d.ts",
364
+ "account/pages/Applications.js",
365
+ "account/pages/Applications.js.map",
366
+ "account/pages/Log.d.ts",
367
+ "account/pages/Log.js",
368
+ "account/pages/Log.js.map",
359
369
  "account/pages/PageProps.d.ts",
360
370
  "account/pages/PageProps.js",
361
371
  "account/pages/PageProps.js.map",
362
372
  "account/pages/Password.d.ts",
363
373
  "account/pages/Password.js",
364
374
  "account/pages/Password.js.map",
375
+ "account/pages/Sessions.d.ts",
376
+ "account/pages/Sessions.js",
377
+ "account/pages/Sessions.js.map",
378
+ "account/pages/Totp.d.ts",
379
+ "account/pages/Totp.js",
380
+ "account/pages/Totp.js.map",
365
381
  "bin/constants.d.ts",
366
382
  "bin/constants.js",
367
383
  "bin/constants.js.map",
@@ -813,8 +829,12 @@
813
829
  "src/account/kcContext/kcContextMocks.ts",
814
830
  "src/account/lib/useGetClassName.ts",
815
831
  "src/account/pages/Account.tsx",
832
+ "src/account/pages/Applications.tsx",
833
+ "src/account/pages/Log.tsx",
816
834
  "src/account/pages/PageProps.ts",
817
835
  "src/account/pages/Password.tsx",
836
+ "src/account/pages/Sessions.tsx",
837
+ "src/account/pages/Totp.tsx",
818
838
  "src/bin/constants.ts",
819
839
  "src/bin/copy-keycloak-resources-to-public.ts",
820
840
  "src/bin/download-builtin-keycloak-theme.ts",
@@ -6,6 +6,10 @@ import { assert, type Equals } from "tsafe/assert";
6
6
 
7
7
  const Password = lazy(() => import("keycloakify/account/pages/Password"));
8
8
  const Account = lazy(() => import("keycloakify/account/pages/Account"));
9
+ const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
10
+ const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
11
+ const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
12
+ const Log = lazy(() => import("keycloakify/account/pages/Log"));
9
13
 
10
14
  export default function Fallback(props: PageProps<KcContext, I18n>) {
11
15
  const { kcContext, ...rest } = props;
@@ -16,8 +20,16 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
16
20
  switch (kcContext.pageId) {
17
21
  case "password.ftl":
18
22
  return <Password kcContext={kcContext} {...rest} />;
23
+ case "sessions.ftl":
24
+ return <Sessions kcContext={kcContext} {...rest} />;
19
25
  case "account.ftl":
20
26
  return <Account kcContext={kcContext} {...rest} />;
27
+ case "totp.ftl":
28
+ return <Totp kcContext={kcContext} {...rest} />;
29
+ case "applications.ftl":
30
+ return <Applications kcContext={kcContext} {...rest} />;
31
+ case "log.ftl":
32
+ return <Log kcContext={kcContext} {...rest} />;
21
33
  }
22
34
  assert<Equals<typeof kcContext, never>>(false);
23
35
  })()}
@@ -11,4 +11,17 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
11
11
  children: ReactNode;
12
12
  };
13
13
 
14
- export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
14
+ export type ClassKey =
15
+ | "kcHtmlClass"
16
+ | "kcBodyClass"
17
+ | "kcButtonClass"
18
+ | "kcButtonPrimaryClass"
19
+ | "kcButtonLargeClass"
20
+ | "kcButtonDefaultClass"
21
+ | "kcContentWrapperClass"
22
+ | "kcFormClass"
23
+ | "kcFormGroupClass"
24
+ | "kcInputWrapperClass"
25
+ | "kcLabelClass"
26
+ | "kcInputClass"
27
+ | "kcInputErrorMessageClass";
@@ -3,7 +3,7 @@ import { assert } from "tsafe/assert";
3
3
  import type { Equals } from "tsafe";
4
4
  import { type ThemeType } from "keycloakify/bin/constants";
5
5
 
6
- export type KcContext = KcContext.Password | KcContext.Account;
6
+ export type KcContext = KcContext.Password | KcContext.Account | KcContext.Sessions | KcContext.Totp | KcContext.Applications | KcContext.Log;
7
7
 
8
8
  export declare namespace KcContext {
9
9
  export type Common = {
@@ -91,6 +91,15 @@ export declare namespace KcContext {
91
91
  username?: string;
92
92
  };
93
93
  properties: Record<string, string | undefined>;
94
+ sessions: {
95
+ sessions: {
96
+ ipAddress: string;
97
+ started?: any;
98
+ lastAccess?: any;
99
+ expires?: any;
100
+ clients: string[];
101
+ }[];
102
+ };
94
103
  };
95
104
 
96
105
  export type Password = Common & {
@@ -112,6 +121,144 @@ export declare namespace KcContext {
112
121
  };
113
122
  stateChecker: string;
114
123
  };
124
+
125
+ export type Sessions = Common & {
126
+ pageId: "sessions.ftl";
127
+ sessions: {
128
+ sessions: {
129
+ ipAddress: string;
130
+ started?: any;
131
+ lastAccess?: any;
132
+ expires?: any;
133
+ clients: string[];
134
+ }[];
135
+ };
136
+ stateChecker: string;
137
+ };
138
+
139
+ export type Totp = Common & {
140
+ pageId: "totp.ftl";
141
+ totp: {
142
+ enabled: boolean;
143
+ totpSecretEncoded: string;
144
+ qrUrl: string;
145
+ policy: {
146
+ algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
147
+ digits: number;
148
+ lookAheadWindow: number;
149
+ } & (
150
+ | {
151
+ type: "totp";
152
+ period: number;
153
+ }
154
+ | {
155
+ type: "hotp";
156
+ initialCounter: number;
157
+ }
158
+ );
159
+ supportedApplications: string[];
160
+ totpSecretQrCode: string;
161
+ manualUrl: string;
162
+ totpSecret: string;
163
+ otpCredentials: { id: string; userLabel: string }[];
164
+ };
165
+ mode?: "qr" | "manual" | undefined | null;
166
+ isAppInitiatedAction: boolean;
167
+ url: {
168
+ accountUrl: string;
169
+ passwordUrl: string;
170
+ totpUrl: string;
171
+ socialUrl: string;
172
+ sessionsUrl: string;
173
+ applicationsUrl: string;
174
+ logUrl: string;
175
+ resourceUrl: string;
176
+ resourcesCommonPath: string;
177
+ resourcesPath: string;
178
+ /** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
179
+ referrerURI?: string;
180
+ getLogoutUrl: () => string;
181
+ };
182
+ stateChecker: string;
183
+ };
184
+
185
+ export type Applications = Common & {
186
+ pageId: "applications.ftl";
187
+ features: {
188
+ log: boolean;
189
+ identityFederation: boolean;
190
+ authorization: boolean;
191
+ passwordUpdateSupported: boolean;
192
+ };
193
+ stateChecker: string;
194
+ applications: {
195
+ applications: {
196
+ realmRolesAvailable: { name: string; description: string }[];
197
+ resourceRolesAvailable: Record<
198
+ string,
199
+ {
200
+ roleName: string;
201
+ roleDescription: string;
202
+ clientName: string;
203
+ clientId: string;
204
+ }[]
205
+ >;
206
+ additionalGrants: string[];
207
+ clientScopesGranted: string[];
208
+ effectiveUrl?: string;
209
+ client: {
210
+ consentScreenText: string;
211
+ surrogateAuthRequired: boolean;
212
+ bearerOnly: boolean;
213
+ id: string;
214
+ protocolMappersStream: Record<string, unknown>;
215
+ includeInTokenScope: boolean;
216
+ redirectUris: string[];
217
+ fullScopeAllowed: boolean;
218
+ registeredNodes: Record<string, unknown>;
219
+ enabled: boolean;
220
+ clientAuthenticatorType: string;
221
+ realmScopeMappingsStream: Record<string, unknown>;
222
+ scopeMappingsStream: Record<string, unknown>;
223
+ displayOnConsentScreen: boolean;
224
+ clientId: string;
225
+ rootUrl: string;
226
+ authenticationFlowBindingOverrides: Record<string, unknown>;
227
+ standardFlowEnabled: boolean;
228
+ attributes: Record<string, unknown>;
229
+ publicClient: boolean;
230
+ alwaysDisplayInConsole: boolean;
231
+ consentRequired: boolean;
232
+ notBefore: string;
233
+ rolesStream: Record<string, unknown>;
234
+ protocol: string;
235
+ dynamicScope: boolean;
236
+ directAccessGrantsEnabled: boolean;
237
+ name: string;
238
+ serviceAccountsEnabled: boolean;
239
+ frontchannelLogout: boolean;
240
+ nodeReRegistrationTimeout: string;
241
+ implicitFlowEnabled: boolean;
242
+ baseUrl: string;
243
+ webOrigins: string[];
244
+ realm: Record<string, unknown>;
245
+ };
246
+ }[];
247
+ };
248
+ };
249
+
250
+ export type Log = Common & {
251
+ pageId: "log.ftl";
252
+ log: {
253
+ events: {
254
+ date: string | number | Date;
255
+ event: string;
256
+ ipAddress: string;
257
+ client: any;
258
+ details: any[];
259
+ }[];
260
+ };
261
+ };
115
262
  }
116
263
 
117
264
  {
@@ -156,6 +156,17 @@ export const kcContextCommonMock: KcContext.Common = {
156
156
  "css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
157
157
  "kcButtonClass": "btn",
158
158
  "kcButtonDefaultClass": "btn-default"
159
+ },
160
+ "sessions": {
161
+ "sessions": [
162
+ {
163
+ "ipAddress": "127.0.0.1",
164
+ "started": new Date().toString(),
165
+ "lastAccess": new Date().toString(),
166
+ "expires": new Date().toString(),
167
+ "clients": ["Chrome", "Firefox"]
168
+ }
169
+ ]
159
170
  }
160
171
  };
161
172
 
@@ -182,5 +193,62 @@ export const kcContextMocks: KcContext[] = [
182
193
  "editUsernameAllowed": true
183
194
  },
184
195
  "stateChecker": ""
196
+ }),
197
+ id<KcContext.Sessions>({
198
+ ...kcContextCommonMock,
199
+ "pageId": "sessions.ftl",
200
+ "sessions": {
201
+ "sessions": [
202
+ {
203
+ ...kcContextCommonMock.sessions,
204
+ "ipAddress": "127.0.0.1",
205
+ "started": new Date().toString(),
206
+ "lastAccess": new Date().toString(),
207
+ "expires": new Date().toString(),
208
+ "clients": ["Chrome", "Firefox"]
209
+ }
210
+ ]
211
+ },
212
+ "stateChecker": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
213
+ }),
214
+ id<KcContext.Totp>({
215
+ ...kcContextCommonMock,
216
+ "pageId": "totp.ftl",
217
+ "totp": {
218
+ "enabled": true,
219
+ "totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
220
+ "qrUrl": "#",
221
+ "totpSecretQrCode":
222
+ "iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
223
+ "manualUrl": "#",
224
+ "totpSecret": "G4nsI8lQagRMUchH8jEG",
225
+ "otpCredentials": [],
226
+ "supportedApplications": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
227
+ "policy": {
228
+ "algorithm": "HmacSHA1",
229
+ "digits": 6,
230
+ "lookAheadWindow": 1,
231
+ "type": "totp",
232
+ "period": 30
233
+ }
234
+ },
235
+ "mode": "qr",
236
+ "isAppInitiatedAction": false,
237
+ "stateChecker": ""
238
+ }),
239
+ id<KcContext.Log>({
240
+ ...kcContextCommonMock,
241
+ "pageId": "log.ftl",
242
+ "log": {
243
+ "events": [
244
+ {
245
+ "date": "2/21/2024, 1:28:39 PM",
246
+ "event": "login",
247
+ "ipAddress": "172.17.0.1",
248
+ "client": "security-admin-console",
249
+ "details": ["auth_method = openid-connect, username = admin"]
250
+ }
251
+ ]
252
+ }
185
253
  })
186
254
  ];
@@ -6,8 +6,15 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
6
6
  "kcHtmlClass": undefined,
7
7
  "kcBodyClass": undefined,
8
8
  "kcButtonClass": "btn",
9
+ "kcContentWrapperClass": "row",
9
10
  "kcButtonPrimaryClass": "btn-primary",
10
11
  "kcButtonLargeClass": "btn-lg",
11
- "kcButtonDefaultClass": "btn-default"
12
+ "kcButtonDefaultClass": "btn-default",
13
+ "kcFormClass": "form-horizontal",
14
+ "kcFormGroupClass": "form-group",
15
+ "kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
16
+ "kcLabelClass": "control-label",
17
+ "kcInputClass": "form-control",
18
+ "kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
12
19
  }
13
20
  });
@@ -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
+ }