feat: Addition of Sessions page

This commit is contained in:
giorgoslytos 2024-02-07 15:18:27 +02:00
parent ca255985c0
commit 22496e36eb
8 changed files with 165 additions and 3 deletions

View File

@ -6,6 +6,7 @@ import { assert, type Equals } from "tsafe/assert";
const Password = lazy(() => import("keycloakify/account/pages/Password")); const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account")); const Account = lazy(() => import("keycloakify/account/pages/Account"));
const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
export default function Fallback(props: PageProps<KcContext, I18n>) { export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props; const { kcContext, ...rest } = props;
@ -16,6 +17,8 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
switch (kcContext.pageId) { switch (kcContext.pageId) {
case "password.ftl": case "password.ftl":
return <Password kcContext={kcContext} {...rest} />; return <Password kcContext={kcContext} {...rest} />;
case "sessions.ftl":
return <Sessions kcContext={kcContext} {...rest} />;
case "account.ftl": case "account.ftl":
return <Account kcContext={kcContext} {...rest} />; return <Account kcContext={kcContext} {...rest} />;
} }

View File

@ -11,4 +11,11 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
children: ReactNode; children: ReactNode;
}; };
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass"; export type ClassKey =
| "kcHtmlClass"
| "kcBodyClass"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonLargeClass"
| "kcButtonDefaultClass"
| "kcContentWrapperClass";

View File

@ -3,7 +3,7 @@ import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import { type ThemeType } from "keycloakify/bin/constants"; import { type ThemeType } from "keycloakify/bin/constants";
export type KcContext = KcContext.Password | KcContext.Account; export type KcContext = KcContext.Password | KcContext.Account | KcContext.Sessions;
export declare namespace KcContext { export declare namespace KcContext {
export type Common = { export type Common = {
@ -90,6 +90,15 @@ export declare namespace KcContext {
lastName?: string; lastName?: string;
username?: string; username?: string;
}; };
sessions: {
sessions: {
ipAddress: string;
started?: any;
lastAccess?: any;
expires?: any;
clients: string[];
}[];
};
}; };
export type Password = Common & { export type Password = Common & {
@ -111,6 +120,20 @@ export declare namespace KcContext {
}; };
stateChecker: string; stateChecker: string;
}; };
export type Sessions = Common & {
pageId: "sessions.ftl";
sessions: {
sessions: {
ipAddress: string;
started?: any;
lastAccess?: any;
expires?: any;
clients: string[];
}[];
};
stateChecker: string;
};
} }
{ {

View File

@ -147,6 +147,17 @@ export const kcContextCommonMock: KcContext.Common = {
"lastName": "doe", "lastName": "doe",
"email": "john.doe@code.gouv.fr", "email": "john.doe@code.gouv.fr",
"username": "doe_j" "username": "doe_j"
},
"sessions": {
"sessions": [
{
"ipAddress": "127.0.0.1",
"started": new Date().toString(),
"lastAccess": new Date().toString(),
"expires": new Date().toString(),
"clients": ["Chrome", "Firefox"]
}
]
} }
}; };
@ -173,5 +184,22 @@ export const kcContextMocks: KcContext[] = [
"editUsernameAllowed": true "editUsernameAllowed": true
}, },
"stateChecker": "" "stateChecker": ""
}),
id<KcContext.Sessions>({
...kcContextCommonMock,
"pageId": "sessions.ftl",
sessions: {
sessions: [
{
...kcContextCommonMock.sessions,
ipAddress: "127.0.0.1",
started: new Date().toString(),
lastAccess: new Date().toString(),
expires: new Date().toString(),
clients: ["Chrome", "Firefox"]
}
]
},
"stateChecker": ""
}) })
]; ];

View File

@ -6,6 +6,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcHtmlClass": undefined, "kcHtmlClass": undefined,
"kcBodyClass": undefined, "kcBodyClass": undefined,
"kcButtonClass": "btn", "kcButtonClass": "btn",
"kcContentWrapperClass": "row",
"kcButtonPrimaryClass": "btn-primary", "kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg", "kcButtonLargeClass": "btn-lg",
"kcButtonDefaultClass": "btn-default" "kcButtonDefaultClass": "btn-default"

View File

@ -0,0 +1,68 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
console.log({ kcContext });
const { url, stateChecker, sessions } = kcContext;
const { msg } = i18n;
console.log({ sdf: kcContext.locale?.supported });
console.log({ asdf: "asdf" });
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
<div className={getClassName("kcContentWrapperClass")}>
<div className="col-md-10">
<h2>{msg("sessionsHtmlTitle")}</h2>
</div>
</div>
<table className="table table-striped table-bordered">
<thead>
<tr>
<th>{msg("ip")}</th>
<th>{msg("started")}</th>
<th>{msg("lastAccess")}</th>
<th>{msg("expires")}</th>
<th>{msg("clients")}</th>
</tr>
</thead>
<tbody role="rowgroup">
{sessions.sessions.map((session, index: number) => (
<tr key={index}>
<td>{session.ipAddress}</td>
<td>{session?.started}</td>
<td>{session?.lastAccess}</td>
<td>{session?.expires}</td>
<td>
{session.clients.map((client: string, clientIndex: number) => (
<div key={clientIndex}>
{client}
<br />
</div>
))}
</td>
</tr>
))}
</tbody>
</table>
<form action={url.sessionsUrl} method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
{msg("doLogOutAllSessions")}
</button>
</form>
</Template>
);
}

View File

@ -27,7 +27,7 @@ export const loginThemePageIds = [
"saml-post-form.ftl" "saml-post-form.ftl"
] as const; ] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const; export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number]; export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number]; export type AccountThemePageId = (typeof accountThemePageIds)[number];

View File

@ -0,0 +1,32 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "sessions.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;
export const WithMessage = () => (
<PageStory
kcContext={{
message: { type: "success", summary: "This is a test message" }
}}
/>
);