keycloakify 9.5.8 → 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 (35) 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/kcContextMocks.js +45 -1
  7. package/account/kcContext/kcContextMocks.js.map +1 -1
  8. package/account/lib/useGetClassName.js +8 -1
  9. package/account/lib/useGetClassName.js.map +1 -1
  10. package/account/pages/Applications.d.ts +7 -0
  11. package/account/pages/Applications.js +24 -0
  12. package/account/pages/Applications.js.map +1 -0
  13. package/account/pages/Log.d.ts +7 -0
  14. package/account/pages/Log.js +13 -0
  15. package/account/pages/Log.js.map +1 -0
  16. package/account/pages/Sessions.d.ts +7 -0
  17. package/account/pages/Sessions.js +18 -0
  18. package/account/pages/Sessions.js.map +1 -0
  19. package/account/pages/Totp.d.ts +7 -0
  20. package/account/pages/Totp.js +20 -0
  21. package/account/pages/Totp.js.map +1 -0
  22. package/bin/keycloakify/generateFtl/pageId.d.ts +1 -1
  23. package/bin/keycloakify/generateFtl/pageId.js +1 -1
  24. package/bin/keycloakify/generateFtl/pageId.js.map +1 -1
  25. package/package.json +21 -1
  26. package/src/account/Fallback.tsx +12 -0
  27. package/src/account/TemplateProps.ts +14 -1
  28. package/src/account/kcContext/KcContext.ts +148 -1
  29. package/src/account/kcContext/kcContextMocks.ts +68 -0
  30. package/src/account/lib/useGetClassName.ts +8 -1
  31. package/src/account/pages/Applications.tsx +138 -0
  32. package/src/account/pages/Log.tsx +70 -0
  33. package/src/account/pages/Sessions.tsx +68 -0
  34. package/src/account/pages/Totp.tsx +236 -0
  35. package/src/bin/keycloakify/generateFtl/pageId.ts +1 -1
@@ -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
+ }