1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Cleanup the session details fragments & load last active IP

This also cleans up the GraphQL scalar types, by making sure we always parse dates correctly
This commit is contained in:
Quentin Gliech
2023-09-21 10:43:52 +02:00
parent 127bf91b54
commit 597482d4d8
28 changed files with 744 additions and 301 deletions

View File

@ -21,6 +21,14 @@ const config: CodegenConfig = {
generates: {
"./src/gql/": {
preset: "client",
config: {
// By default, unknown scalars are generated as `any`. This is not ideal for catching potential bugs.
defaultScalarType: "unknown",
scalars: {
DateTime: "string",
Url: "string",
},
},
},
"./src/gql/schema.ts": {
plugins: ["urql-introspection"],

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { atom, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
@ -29,12 +30,14 @@ import { useCurrentBrowserSessionId } from "../utils/session/useCurrentBrowserSe
import EndSessionButton from "./Session/EndSessionButton";
import Session from "./Session/Session";
export const BROWSER_SESSION_FRAGMENT = graphql(/* GraphQL */ `
const FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_session on BrowserSession {
id
createdAt
finishedAt
userAgent
lastActiveIp
lastActiveAt
lastAuthentication {
id
createdAt
@ -92,17 +95,21 @@ export const useEndBrowserSession = (
};
type Props = {
session: FragmentType<typeof BROWSER_SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
};
const BrowserSession: React.FC<Props> = ({ session }) => {
const currentBrowserSessionId = useCurrentBrowserSessionId();
const data = useFragment(BROWSER_SESSION_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const isCurrent = data.id === currentBrowserSessionId;
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
const createdAt = data.createdAt;
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
const deviceInformation = parseUserAgent(data.userAgent || undefined);
const sessionName =
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
@ -115,8 +122,10 @@ const BrowserSession: React.FC<Props> = ({ session }) => {
id={data.id}
name={name}
createdAt={createdAt}
finishedAt={data.finishedAt}
finishedAt={finishedAt}
isCurrent={isCurrent}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>

View File

@ -70,7 +70,7 @@ const OAuth2ClientDetail: React.FC<Props> = ({ client }) => {
<BlockList>
<header className={styles.header}>
<ClientAvatar
logoUri={data.logoUri}
logoUri={data.logoUri || undefined}
name={data.clientName || data.clientId}
size="1.5rem"
/>

View File

@ -17,28 +17,30 @@
import { create } from "react-test-renderer";
import { describe, expect, it, beforeAll } from "vitest";
import { FragmentType } from "../gql/fragment-masking";
import { makeFragmentData } from "../gql";
import { WithLocation } from "../test-utils/WithLocation";
import { mockLocale } from "../test-utils/mockLocale";
import CompatSession, { COMPAT_SESSION_FRAGMENT } from "./CompatSession";
import CompatSession, { FRAGMENT } from "./CompatSession";
describe("<CompatSession />", () => {
const session = {
const baseSession = {
id: "session-id",
deviceId: "abcd1234",
createdAt: "2023-06-29T03:35:17.451292+00:00",
lastActiveIp: "1.2.3.4",
ssoLogin: {
id: "test-id",
redirectUri: "https://element.io",
},
} as FragmentType<typeof COMPAT_SESSION_FRAGMENT>;
};
const finishedAt = "2023-06-29T03:35:19.451292+00:00";
beforeAll(() => mockLocale());
it("renders an active session", () => {
const session = makeFragmentData(baseSession, FRAGMENT);
const component = create(
<WithLocation>
<CompatSession session={session} />
@ -48,13 +50,16 @@ describe("<CompatSession />", () => {
});
it("renders a finished session", () => {
const finishedSession = {
...session,
finishedAt,
};
const session = makeFragmentData(
{
...baseSession,
finishedAt,
},
FRAGMENT,
);
const component = create(
<WithLocation>
<CompatSession session={finishedSession} />
<CompatSession session={session} />
</WithLocation>,
);
expect(component.toJSON()).toMatchSnapshot();

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { atom, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
@ -22,12 +23,14 @@ import { Link } from "../routing";
import { Session } from "./Session";
import EndSessionButton from "./Session/EndSessionButton";
export const COMPAT_SESSION_FRAGMENT = graphql(/* GraphQL */ `
export const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_session on CompatSession {
id
createdAt
deviceId
finishedAt
lastActiveIp
lastActiveAt
ssoLogin {
id
redirectUri
@ -81,9 +84,9 @@ export const simplifyUrl = (url: string): string => {
};
const CompatSession: React.FC<{
session: FragmentType<typeof COMPAT_SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
}> = ({ session }) => {
const data = useFragment(COMPAT_SESSION_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const endCompatSession = useSetAtom(endCompatSessionFamily(data.id));
const onSessionEnd = async (): Promise<void> => {
@ -98,13 +101,21 @@ const CompatSession: React.FC<{
? simplifyUrl(data.ssoLogin.redirectUri)
: undefined;
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
return (
<Session
id={data.id}
name={sessionName}
createdAt={data.createdAt}
finishedAt={data.finishedAt || undefined}
createdAt={createdAt}
finishedAt={finishedAt}
clientName={clientName}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>

View File

@ -17,26 +17,25 @@
import { create } from "react-test-renderer";
import { describe, expect, it, beforeAll } from "vitest";
import { FragmentType } from "../gql/fragment-masking";
import { makeFragmentData } from "../gql";
import { WithLocation } from "../test-utils/WithLocation";
import { mockLocale } from "../test-utils/mockLocale";
import OAuth2Session, { OAUTH2_SESSION_FRAGMENT } from "./OAuth2Session";
import OAuth2Session, { FRAGMENT } from "./OAuth2Session";
describe("<OAuth2Session />", () => {
const defaultProps = {
session: {
id: "session-id",
scope:
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234",
createdAt: "2023-06-29T03:35:17.451292+00:00",
client: {
id: "test-id",
clientId: "test-client-id",
clientName: "Element",
clientUri: "https://element.io",
},
} as FragmentType<typeof OAUTH2_SESSION_FRAGMENT>,
const defaultSession = {
id: "session-id",
scope:
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234",
createdAt: "2023-06-29T03:35:17.451292+00:00",
lastActiveIp: "1.2.3.4",
client: {
id: "test-id",
clientId: "test-client-id",
clientName: "Element",
clientUri: "https://element.io",
},
};
const finishedAt = "2023-06-29T03:35:19.451292+00:00";
@ -44,22 +43,27 @@ describe("<OAuth2Session />", () => {
beforeAll(() => mockLocale());
it("renders an active session", () => {
const session = makeFragmentData(defaultSession, FRAGMENT);
const component = create(
<WithLocation>
<OAuth2Session {...defaultProps} />
<OAuth2Session session={session} />
</WithLocation>,
);
expect(component.toJSON()).toMatchSnapshot();
});
it("renders a finished session", () => {
const finishedSession = {
...defaultProps.session,
finishedAt,
};
const session = makeFragmentData(
{
...defaultSession,
finishedAt,
},
FRAGMENT,
);
const component = create(
<WithLocation>
<OAuth2Session session={finishedSession} />
<OAuth2Session session={session} />
</WithLocation>,
);
expect(component.toJSON()).toMatchSnapshot();

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { atom, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
@ -23,36 +24,23 @@ import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
import { Session } from "./Session";
import EndSessionButton from "./Session/EndSessionButton";
export const OAUTH2_SESSION_FRAGMENT = graphql(/* GraphQL */ `
export const FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Session_session on Oauth2Session {
id
scope
createdAt
finishedAt
lastActiveIp
lastActiveAt
client {
id
clientId
clientName
clientUri
logoUri
}
}
`);
export type Oauth2SessionType = {
id: string;
scope: string;
createdAt: string;
finishedAt: string | null;
client: {
id: string;
clientId: string;
clientName: string;
clientUri: string;
logoUri: string | null;
};
};
const END_SESSION_MUTATION = graphql(/* GraphQL */ `
mutation EndOAuth2Session($id: ID!) {
endOauth2Session(input: { oauth2SessionId: $id }) {
@ -78,14 +66,11 @@ export const endSessionFamily = atomFamily((id: string) => {
});
type Props = {
session: FragmentType<typeof OAUTH2_SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
};
const OAuth2Session: React.FC<Props> = ({ session }) => {
const data = useFragment(
OAUTH2_SESSION_FRAGMENT,
session,
) as Oauth2SessionType;
const data = useFragment(FRAGMENT, session);
const endSession = useSetAtom(endSessionFamily(data.id));
const onSessionEnd = async (): Promise<void> => {
@ -98,14 +83,22 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
<Link route={{ type: "session", id: deviceId }}>{deviceId}</Link>
);
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
return (
<Session
id={data.id}
name={name}
createdAt={data.createdAt}
finishedAt={data.finishedAt || undefined}
clientName={data.client.clientName}
createdAt={createdAt}
finishedAt={finishedAt}
clientName={data.client.clientName || data.client.clientId || undefined}
clientLogoUri={data.client.logoUri || undefined}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>

View File

@ -13,40 +13,44 @@
// limitations under the License.
import type { Meta, StoryObj } from "@storybook/react";
import { parseISO, subDays, subHours } from "date-fns";
import LastActive from "./LastActive";
type Props = {
lastActiveTimestamp: number;
now: number;
};
const Template: React.FC<Props> = ({ lastActiveTimestamp, now }) => {
return <LastActive lastActiveTimestamp={lastActiveTimestamp} now={now} />;
};
const meta = {
title: "UI/Session/Last active time",
component: Template,
component: LastActive,
argTypes: {
lastActive: { control: { type: "date" } },
now: { control: { type: "date" } },
},
tags: ["autodocs"],
} satisfies Meta<typeof Template>;
} satisfies Meta<typeof LastActive>;
export default meta;
type Story = StoryObj<typeof Template>;
type Story = StoryObj<typeof LastActive>;
const now = 1694999531800;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const now = parseISO("2023-09-18T01:12:00.000Z");
export const Basic: Story = {
args: {
// yesterday
lastActiveTimestamp: now - ONE_DAY_MS,
// An hour ago
lastActive: subHours(now, 1),
now,
},
};
export const ActiveThreeDaysAgo: Story = {
args: {
// Three days ago
lastActive: subDays(now, 3),
now,
},
};
export const ActiveNow: Story = {
args: {
lastActiveTimestamp: now - 1000,
lastActive: now,
now,
},
};
@ -54,7 +58,7 @@ export const ActiveNow: Story = {
export const Inactive: Story = {
args: {
// 91 days ago
lastActiveTimestamp: now - 91 * ONE_DAY_MS,
lastActive: subDays(now, 91),
now,
},
};

View File

@ -20,7 +20,12 @@ import { describe, afterEach, expect, it, beforeAll } from "vitest";
import { mockLocale } from "../../test-utils/mockLocale";
import Meta, { ActiveNow, Basic, Inactive } from "./LastActive.stories";
import Meta, {
ActiveNow,
ActiveThreeDaysAgo,
Basic,
Inactive,
} from "./LastActive.stories";
describe("<LastActive", () => {
beforeAll(() => mockLocale());
@ -33,6 +38,12 @@ describe("<LastActive", () => {
});
it("renders a default timestamp", () => {
const Component = composeStory(ActiveThreeDaysAgo, Meta);
const { container } = render(<Component />);
expect(container).toMatchSnapshot();
});
it("renders a relative timestamp", () => {
const Component = composeStory(Basic, Meta);
const { container } = render(<Component />);
expect(container).toMatchSnapshot();

View File

@ -12,35 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { differenceInSeconds, parseISO } from "date-fns";
import { formatDate, formatReadableDate } from "../DateTime";
import styles from "./LastActive.module.css";
// 3 minutes
const ACTIVE_NOW_MAX_AGE = 1000 * 60 * 3;
const ACTIVE_NOW_MAX_AGE = 60 * 3;
/// 90 days
const INACTIVE_MIN_AGE = 1000 * 60 * 60 * 24 * 90;
const INACTIVE_MIN_AGE = 60 * 60 * 24 * 90;
const LastActive: React.FC<{ lastActiveTimestamp: number; now?: number }> = ({
lastActiveTimestamp,
now: nowProps,
}) => {
const now = nowProps || Date.now();
const formattedDate = formatDate(new Date(lastActiveTimestamp));
if (lastActiveTimestamp >= now - ACTIVE_NOW_MAX_AGE) {
const LastActive: React.FC<{
lastActive: Date | string;
now?: Date | string;
}> = ({ lastActive: lastActiveProps, now: nowProps }) => {
const lastActive =
typeof lastActiveProps === "string"
? parseISO(lastActiveProps)
: lastActiveProps;
const now = nowProps
? typeof nowProps === "string"
? parseISO(nowProps)
: nowProps
: new Date();
const formattedDate = formatDate(lastActive);
if (differenceInSeconds(now, lastActive) <= ACTIVE_NOW_MAX_AGE) {
return (
<span title={formattedDate} className={styles.active}>
Active now
</span>
);
}
if (lastActiveTimestamp < now - INACTIVE_MIN_AGE) {
if (differenceInSeconds(now, lastActive) > INACTIVE_MIN_AGE) {
return <span title={formattedDate}>Inactive for 90+ days</span>;
}
const relativeDate = formatReadableDate(
new Date(lastActiveTimestamp),
new Date(now),
);
const relativeDate = formatReadableDate(lastActive, now);
return <span title={formattedDate}>{`Active ${relativeDate}`}</span>;
};

View File

@ -14,20 +14,24 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { ReactElement } from "react";
import BlockList from "../BlockList/BlockList";
import Session, { SessionProps } from "./Session";
const Template: React.FC<React.PropsWithChildren<SessionProps>> = (props) => {
return <Session {...props} />;
};
import Session from "./Session";
const meta = {
title: "UI/Session/Session",
component: Template,
component: Session,
tags: ["autodocs"],
argTypes: {
createdAt: { control: { type: "date" } },
finishedAt: { control: { type: "date" } },
lastActiveAt: { control: { type: "date" } },
},
decorators: [
(Story): ReactElement => (
<div style={{ width: "378px" }}>
@ -37,21 +41,22 @@ const meta = {
</div>
),
],
} satisfies Meta<typeof Template>;
} satisfies Meta<typeof Session>;
export default meta;
type Story = StoryObj<typeof Template>;
type Story = StoryObj<typeof Session>;
const defaultProps = {
id: "oauth2_session:01H5VAGA5NYTKJVXP3HMMKDJQ0",
createdAt: "2023-06-29T03:35:17.451292+00:00",
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
};
export const BasicSession: Story = {
args: {
...defaultProps,
name: "KlTqK9CRt3",
ipAddress: "2001:8003:c4614:f501:3091:888a:49c7",
lastActiveIp: "2001:8003:c4614:f501:3091:888a:49c7",
lastActiveAt: parseISO("2023-07-29T03:35:17.451292+00:00"),
clientName: "Element",
},
};
@ -60,7 +65,7 @@ export const BasicFinishedSession: Story = {
args: {
...defaultProps,
name: "Chrome on Android",
finishedAt: "2023-06-30T03:35:17.451292+00:00",
finishedAt: parseISO("2023-06-30T03:35:17.451292+00:00"),
},
};

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { create } from "react-test-renderer";
import { describe, expect, it, beforeAll } from "vitest";
@ -22,10 +23,10 @@ import Session from "./Session";
describe("<Session />", () => {
const defaultProps = {
id: "session-id",
createdAt: "2023-06-29T03:35:17.451292+00:00",
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
};
const finishedAt = "2023-06-29T03:35:19.451292+00:00";
const finishedAt = parseISO("2023-06-29T03:35:19.451292+00:00");
beforeAll(() => mockLocale());
@ -69,7 +70,7 @@ describe("<Session />", () => {
{...defaultProps}
finishedAt={finishedAt}
clientName={clientName}
ipAddress="127.0.0.1"
lastActiveIp="127.0.0.1"
/>,
);
expect(component.toJSON()).toMatchSnapshot();

View File

@ -19,21 +19,23 @@ import Block from "../Block";
import DateTime from "../DateTime";
import ClientAvatar from "./ClientAvatar";
import LastActive from "./LastActive";
import styles from "./Session.module.css";
const SessionMetadata: React.FC<React.ComponentProps<typeof Body>> = (
props,
) => <Body {...props} size="sm" className={styles.sessionMetadata} />;
export type SessionProps = {
type SessionProps = {
id: string;
name?: string | ReactNode;
createdAt: string;
finishedAt?: string;
createdAt: Date;
finishedAt?: Date;
clientName?: string;
clientLogoUri?: string;
isCurrent?: boolean;
ipAddress?: string;
lastActiveIp?: string;
lastActiveAt?: Date;
};
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
id,
@ -42,7 +44,8 @@ const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
finishedAt,
clientName,
clientLogoUri,
ipAddress,
lastActiveIp,
lastActiveAt,
isCurrent,
children,
}) => {
@ -60,7 +63,12 @@ const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
Finished <DateTime datetime={finishedAt} />
</SessionMetadata>
)}
{!!ipAddress && <SessionMetadata>{ipAddress}</SessionMetadata>}
{!!lastActiveAt && (
<SessionMetadata>
<LastActive lastActive={lastActiveAt} />
</SessionMetadata>
)}
{!!lastActiveIp && <SessionMetadata>{lastActiveIp}</SessionMetadata>}
{!!clientName && (
<SessionMetadata>
<ClientAvatar

View File

@ -3,9 +3,19 @@
exports[`<LastActive > renders a default timestamp 1`] = `
<div>
<span
title="Sun, 17 Sept 2023, 01:12"
title="Fri, 15 Sept 2023, 01:12"
>
Active Sun, 17 Sept 2023, 01:12
Active Fri, 15 Sept 2023, 01:12
</span>
</div>
`;
exports[`<LastActive > renders a relative timestamp 1`] = `
<div>
<span
title="Mon, 18 Sept 2023, 00:12"
>
Active 1 hour ago
</span>
</div>
`;

View File

@ -13,9 +13,9 @@
// limitations under the License.
import { Badge } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { FragmentType, useFragment } from "../../gql";
import { BROWSER_SESSION_DETAIL_FRAGMENT } from "../../pages/BrowserSession";
import { FragmentType, graphql, useFragment } from "../../gql";
import {
parseUserAgent,
sessionNameFromDeviceInformation,
@ -25,17 +25,37 @@ import BlockList from "../BlockList/BlockList";
import { useEndBrowserSession } from "../BrowserSession";
import DateTime from "../DateTime";
import EndSessionButton from "../Session/EndSessionButton";
import LastActive from "../Session/LastActive";
import styles from "./BrowserSessionDetail.module.css";
import SessionDetails from "./SessionDetails";
import SessionHeader from "./SessionHeader";
const FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_detail on BrowserSession {
id
createdAt
finishedAt
userAgent
lastActiveIp
lastActiveAt
lastAuthentication {
id
createdAt
}
user {
id
username
}
}
`);
type Props = {
session: FragmentType<typeof BROWSER_SESSION_DETAIL_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
};
const BrowserSessionDetail: React.FC<Props> = ({ session }) => {
const data = useFragment(BROWSER_SESSION_DETAIL_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const currentBrowserSessionId = useCurrentBrowserSessionId();
const isCurrent = currentBrowserSessionId === data.id;
@ -46,18 +66,34 @@ const BrowserSessionDetail: React.FC<Props> = ({ session }) => {
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
const finishedAt = data.finishedAt
? [{ label: "Finished", value: <DateTime datetime={data.finishedAt} /> }]
? [
{
label: "Finished",
value: <DateTime datetime={parseISO(data.finishedAt)} />,
},
]
: [];
const ipAddress = data.ipAddress
? [{ label: "IP Address", value: <code>{data.ipAddress}</code> }]
const lastActiveIp = data.lastActiveIp
? [{ label: "IP Address", value: <code>{data.lastActiveIp}</code> }]
: [];
const lastActiveAt = data.lastActiveAt
? [
{
label: "Last Active",
value: <LastActive lastActive={parseISO(data.lastActiveAt)} />,
},
]
: [];
const lastAuthentication = data.lastAuthentication
? [
{
label: "Last Authentication",
value: <DateTime datetime={data.lastAuthentication.createdAt} />,
value: (
<DateTime datetime={parseISO(data.lastAuthentication.createdAt)} />
),
},
]
: [];
@ -68,7 +104,8 @@ const BrowserSessionDetail: React.FC<Props> = ({ session }) => {
{ label: "User Name", value: <code>{data.user.username}</code> },
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
...finishedAt,
...ipAddress,
...lastActiveAt,
...lastActiveIp,
...lastAuthentication,
];

View File

@ -17,18 +17,19 @@
import { render, cleanup } from "@testing-library/react";
import { describe, expect, it, afterEach, beforeAll } from "vitest";
import { makeFragmentData } from "../../gql/fragment-masking";
import { makeFragmentData } from "../../gql";
import { WithLocation } from "../../test-utils/WithLocation";
import { mockLocale } from "../../test-utils/mockLocale";
import { COMPAT_SESSION_FRAGMENT } from "../CompatSession";
import CompatSessionDetail from "./CompatSessionDetail";
import CompatSessionDetail, { FRAGMENT } from "./CompatSessionDetail";
describe("<CompatSessionDetail>", () => {
const baseSession = {
id: "session-id",
deviceId: "abcd1234",
createdAt: "2023-06-29T03:35:17.451292+00:00",
lastActiveIp: "1.2.3.4",
lastActiveAt: "2023-07-29T03:35:17.451292+00:00",
ssoLogin: {
id: "test-id",
redirectUri: "https://element.io",
@ -39,7 +40,7 @@ describe("<CompatSessionDetail>", () => {
afterEach(cleanup);
it("renders a compatability session details", () => {
const data = makeFragmentData(baseSession, COMPAT_SESSION_FRAGMENT);
const data = makeFragmentData(baseSession, FRAGMENT);
const { container } = render(
<WithLocation>
@ -50,16 +51,13 @@ describe("<CompatSessionDetail>", () => {
expect(container).toMatchSnapshot();
});
it("renders a compatability session without an ssoLogin redirectUri", () => {
it("renders a compatability session without an ssoLogin", () => {
const data = makeFragmentData(
{
...baseSession,
ssoLogin: {
id: "dfsdjfdk",
redirectUri: undefined,
},
ssoLogin: null,
},
COMPAT_SESSION_FRAGMENT,
FRAGMENT,
);
const { container } = render(
@ -77,7 +75,7 @@ describe("<CompatSessionDetail>", () => {
...baseSession,
finishedAt: "2023-07-29T03:35:17.451292+00:00",
},
COMPAT_SESSION_FRAGMENT,
FRAGMENT,
);
const { getByText, queryByText } = render(

View File

@ -12,28 +12,41 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { useSetAtom } from "jotai";
import { FragmentType, useFragment } from "../../gql";
import { FragmentType, graphql, useFragment } from "../../gql";
import BlockList from "../BlockList/BlockList";
import {
COMPAT_SESSION_FRAGMENT,
endCompatSessionFamily,
simplifyUrl,
} from "../CompatSession";
import { endCompatSessionFamily, simplifyUrl } from "../CompatSession";
import DateTime from "../DateTime";
import ExternalLink from "../ExternalLink/ExternalLink";
import EndSessionButton from "../Session/EndSessionButton";
import LastActive from "../Session/LastActive";
import SessionDetails from "./SessionDetails";
import SessionHeader from "./SessionHeader";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_detail on CompatSession {
id
createdAt
deviceId
finishedAt
lastActiveIp
lastActiveAt
ssoLogin {
id
redirectUri
}
}
`);
type Props = {
session: FragmentType<typeof COMPAT_SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
};
const CompatSessionDetail: React.FC<Props> = ({ session }) => {
const data = useFragment(COMPAT_SESSION_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const endSession = useSetAtom(endCompatSessionFamily(data.id));
const onSessionEnd = async (): Promise<void> => {
@ -41,19 +54,37 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
};
const finishedAt = data.finishedAt
? [{ label: "Finished", value: <DateTime datetime={data.finishedAt} /> }]
? [
{
label: "Finished",
value: <DateTime datetime={parseISO(data.finishedAt)} />,
},
]
: [];
const ipAddress = data.ipAddress
? [{ label: "IP Address", value: <code>{data.ipAddress}</code> }]
const lastActiveIp = data.lastActiveIp
? [{ label: "IP Address", value: <code>{data.lastActiveIp}</code> }]
: [];
const lastActiveAt = data.lastActiveAt
? [
{
label: "Last Active",
value: <LastActive lastActive={parseISO(data.lastActiveAt)} />,
},
]
: [];
const sessionDetails = [
{ label: "ID", value: <code>{data.id}</code> },
{ label: "Device ID", value: <code>{data.deviceId}</code> },
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
{
label: "Signed in",
value: <DateTime datetime={parseISO(data.createdAt)} />,
},
...finishedAt,
...ipAddress,
...lastActiveAt,
...lastActiveIp,
];
const clientDetails: { label: string; value: string | JSX.Element }[] = [];

View File

@ -17,12 +17,11 @@
import { render, cleanup } from "@testing-library/react";
import { describe, expect, it, afterEach, beforeAll } from "vitest";
import { makeFragmentData } from "../../gql/fragment-masking";
import { makeFragmentData } from "../../gql";
import { WithLocation } from "../../test-utils/WithLocation";
import { mockLocale } from "../../test-utils/mockLocale";
import { OAUTH2_SESSION_FRAGMENT } from "../OAuth2Session";
import OAuth2SessionDetail from "./OAuth2SessionDetail";
import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail";
describe("<OAuth2SessionDetail>", () => {
const baseSession = {
@ -30,6 +29,8 @@ describe("<OAuth2SessionDetail>", () => {
scope:
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234",
createdAt: "2023-06-29T03:35:17.451292+00:00",
lastActiveAt: "2023-07-29T03:35:17.451292+00:00",
lastActiveIp: "1.2.3.4",
client: {
id: "test-id",
clientId: "test-client-id",
@ -42,7 +43,7 @@ describe("<OAuth2SessionDetail>", () => {
afterEach(cleanup);
it("renders session details", () => {
const data = makeFragmentData(baseSession, OAUTH2_SESSION_FRAGMENT);
const data = makeFragmentData(baseSession, FRAGMENT);
const { container } = render(
<WithLocation>
@ -59,7 +60,7 @@ describe("<OAuth2SessionDetail>", () => {
...baseSession,
finishedAt: "2023-07-29T03:35:17.451292+00:00",
},
OAUTH2_SESSION_FRAGMENT,
FRAGMENT,
);
const { getByText, queryByText } = render(

View File

@ -12,26 +12,46 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { parseISO } from "date-fns";
import { useSetAtom } from "jotai";
import { FragmentType, useFragment } from "../../gql";
import { FragmentType, graphql, useFragment } from "../../gql";
import { Link } from "../../routing";
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
import BlockList from "../BlockList/BlockList";
import DateTime from "../DateTime";
import { OAUTH2_SESSION_FRAGMENT, endSessionFamily } from "../OAuth2Session";
import { endSessionFamily } from "../OAuth2Session";
import ClientAvatar from "../Session/ClientAvatar";
import EndSessionButton from "../Session/EndSessionButton";
import LastActive from "../Session/LastActive";
import SessionDetails from "./SessionDetails";
import SessionHeader from "./SessionHeader";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Session_detail on Oauth2Session {
id
scope
createdAt
finishedAt
lastActiveIp
lastActiveAt
client {
id
clientId
clientName
clientUri
logoUri
}
}
`);
type Props = {
session: FragmentType<typeof OAUTH2_SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
};
const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
const data = useFragment(OAUTH2_SESSION_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const endSession = useSetAtom(endSessionFamily(data.id));
const onSessionEnd = async (): Promise<void> => {
@ -43,11 +63,25 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
const scopes = data.scope.split(" ");
const finishedAt = data.finishedAt
? [{ label: "Finished", value: <DateTime datetime={data.createdAt} /> }]
? [
{
label: "Finished",
value: <DateTime datetime={parseISO(data.createdAt)} />,
},
]
: [];
const ipAddress = data.ipAddress
? [{ label: "IP Address", value: <code>{data.ipAddress}</code> }]
const lastActiveIp = data.lastActiveIp
? [{ label: "IP Address", value: <code>{data.lastActiveIp}</code> }]
: [];
const lastActiveAt = data.lastActiveAt
? [
{
label: "Last Active",
value: <LastActive lastActive={parseISO(data.lastActiveAt)} />,
},
]
: [];
const sessionDetails = [
@ -55,15 +89,16 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
{ label: "Device ID", value: <code>{deviceId}</code> },
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
...finishedAt,
...ipAddress,
...lastActiveAt,
...lastActiveIp,
{
label: "Scopes",
value: (
<div>
<span>
{scopes.map((scope) => (
<code key={scope}>{scope}</code>
))}
</div>
</span>
),
},
];
@ -89,7 +124,7 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
{
label: "Uri",
value: (
<a target="_blank" href={data.client.clientUri}>
<a target="_blank" href={data.client.clientUri || undefined}>
{data.client.clientUri}
</a>
),

View File

@ -28,8 +28,8 @@ const QUERY = graphql(/* GraphQL */ `
query SessionQuery($userId: ID!, $deviceId: String!) {
session(userId: $userId, deviceId: $deviceId) {
__typename
...CompatSession_session
...OAuth2Session_session
...CompatSession_detail
...OAuth2Session_detail
}
}
`);
@ -45,6 +45,11 @@ const sessionFamily = atomFamily(
},
);
// A type-safe way to ensure we've handled all session types
const unknownSessionType = (type: never): never => {
throw new Error(`Unknown session type: ${type}`);
};
const SessionDetail: React.FC<{
deviceId: string;
userId: string;
@ -70,10 +75,13 @@ const SessionDetail: React.FC<{
const sessionType = session.__typename;
if (sessionType === "Oauth2Session") {
return <OAuth2SessionDetail session={session} />;
} else {
return <CompatSessionDetail session={session} />;
switch (sessionType) {
case "CompatSession":
return <CompatSessionDetail session={session} />;
case "Oauth2Session":
return <OAuth2SessionDetail session={session} />;
default:
unknownSessionType(sessionType);
}
};

View File

@ -94,6 +94,40 @@ exports[`<CompatSessionDetail> > renders a compatability session details 1`] = `
</time>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
Last Active
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<span
title="Sat, 29 Jul 2023, 03:35"
>
Active Sat, 29 Jul 2023, 03:35
</span>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
IP Address
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<code>
1.2.3.4
</code>
</p>
</li>
</ul>
</div>
<div
@ -163,7 +197,7 @@ exports[`<CompatSessionDetail> > renders a compatability session details 1`] = `
</div>
`;
exports[`<CompatSessionDetail> > renders a compatability session without an ssoLogin redirectUri 1`] = `
exports[`<CompatSessionDetail> > renders a compatability session without an ssoLogin 1`] = `
<div>
<div
class="_blockList_f8cc7f"
@ -257,6 +291,40 @@ exports[`<CompatSessionDetail> > renders a compatability session without an ssoL
</time>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
Last Active
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<span
title="Sat, 29 Jul 2023, 03:35"
>
Active Sat, 29 Jul 2023, 03:35
</span>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
IP Address
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<code>
1.2.3.4
</code>
</p>
</li>
</ul>
</div>
<button

View File

@ -94,6 +94,40 @@ exports[`<OAuth2SessionDetail> > renders session details 1`] = `
</time>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
Last Active
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<span
title="Sat, 29 Jul 2023, 03:35"
>
Active Sat, 29 Jul 2023, 03:35
</span>
</p>
</li>
<li
class="_detailRow_040867"
>
<p
class="_font-body-sm-semibold_1jx6b_45 _detailLabel_040867"
>
IP Address
</p>
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<code>
1.2.3.4
</code>
</p>
</li>
<li
class="_detailRow_040867"
>
@ -105,7 +139,7 @@ exports[`<OAuth2SessionDetail> > renders session details 1`] = `
<p
class="_font-body-sm-regular_1jx6b_40 _detailValue_040867"
>
<div>
<span>
<code>
openid
</code>
@ -115,7 +149,7 @@ exports[`<OAuth2SessionDetail> > renders session details 1`] = `
<code>
urn:matrix:org.matrix.msc2967.client:device:abcd1234
</code>
</div>
</span>
</p>
</li>
</ul>

View File

@ -37,6 +37,11 @@ exports[`<CompatSession /> > renders a finished session 1`] = `
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
@ -76,6 +81,11 @@ exports[`<CompatSession /> > renders an active session 1`] = `
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>

View File

@ -37,6 +37,11 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
@ -76,6 +81,11 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_font-body-sm-regular_1jx6b_40 _sessionMetadata_634806"
>

View File

@ -17,7 +17,7 @@ const documents = {
types.CurrentViewerQueryDocument,
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n":
types.CurrentViewerSessionQueryDocument,
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n }\n":
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
types.BrowserSession_SessionFragmentDoc,
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
types.EndBrowserSessionDocument,
@ -25,15 +25,21 @@ const documents = {
types.BrowserSessionListDocument,
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
types.OAuth2Client_DetailFragmentDoc,
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
types.CompatSession_SessionFragmentDoc,
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
types.EndCompatSessionDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n":
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n logoUri\n }\n }\n":
types.OAuth2Session_SessionFragmentDoc,
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
types.EndOAuth2SessionDocument,
"\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n":
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n":
types.BrowserSession_DetailFragmentDoc,
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
types.CompatSession_DetailFragmentDoc,
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n":
types.OAuth2Session_DetailFragmentDoc,
"\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n }\n }\n":
types.SessionQueryDocument,
"\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n":
types.UnverifiedEmailAlertFragmentDoc,
@ -63,8 +69,6 @@ const documents = {
types.VerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.ResendVerificationEmailDocument,
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n":
types.BrowserSession_DetailFragmentDoc,
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n ...BrowserSession_detail\n }\n }\n":
types.BrowserSessionQueryDocument,
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
@ -105,8 +109,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n }\n",
): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n",
): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -129,8 +133,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n",
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n",
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -141,8 +145,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n",
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"];
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n logoUri\n }\n }\n",
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n logoUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -153,8 +157,26 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n",
): (typeof documents)["\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n"];
source: "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n",
): (typeof documents)["\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n",
): (typeof documents)["\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n",
): (typeof documents)["\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n }\n }\n",
): (typeof documents)["\n query SessionQuery($userId: ID!, $deviceId: String!) {\n session(userId: $userId, deviceId: $deviceId) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -239,12 +261,6 @@ export function graphql(
export function graphql(
source: "\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n",
): (typeof documents)["\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@ -32,9 +32,9 @@ export type Scalars = {
*
* The input/output is a string in RFC3339 format.
*/
DateTime: { input: any; output: any };
DateTime: { input: string; output: string };
/** URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/) */
Url: { input: any; output: any };
Url: { input: string; output: string };
};
/** The input for the `addEmail` mutation */
@ -1083,13 +1083,15 @@ export type CurrentViewerSessionQueryQuery = {
export type BrowserSession_SessionFragment = {
__typename?: "BrowserSession";
id: string;
createdAt: any;
finishedAt?: any | null;
createdAt: string;
finishedAt?: string | null;
userAgent?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
lastAuthentication?: {
__typename?: "Authentication";
id: string;
createdAt: any;
createdAt: string;
} | null;
} & { " $fragmentName"?: "BrowserSession_SessionFragment" };
@ -1154,23 +1156,25 @@ export type OAuth2Client_DetailFragment = {
id: string;
clientId: string;
clientName?: string | null;
clientUri?: any | null;
logoUri?: any | null;
tosUri?: any | null;
policyUri?: any | null;
redirectUris: Array<any>;
clientUri?: string | null;
logoUri?: string | null;
tosUri?: string | null;
policyUri?: string | null;
redirectUris: Array<string>;
} & { " $fragmentName"?: "OAuth2Client_DetailFragment" };
export type CompatSession_SessionFragment = {
__typename?: "CompatSession";
id: string;
createdAt: any;
createdAt: string;
deviceId: string;
finishedAt?: any | null;
finishedAt?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
ssoLogin?: {
__typename?: "CompatSsoLogin";
id: string;
redirectUri: any;
redirectUri: string;
} | null;
} & { " $fragmentName"?: "CompatSession_SessionFragment" };
@ -1186,7 +1190,7 @@ export type EndCompatSessionMutation = {
compatSession?: {
__typename?: "CompatSession";
id: string;
finishedAt?: any | null;
finishedAt?: string | null;
} | null;
};
};
@ -1195,15 +1199,16 @@ export type OAuth2Session_SessionFragment = {
__typename?: "Oauth2Session";
id: string;
scope: string;
createdAt: any;
finishedAt?: any | null;
createdAt: string;
finishedAt?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
client: {
__typename?: "Oauth2Client";
id: string;
clientId: string;
clientName?: string | null;
clientUri?: any | null;
logoUri?: any | null;
logoUri?: string | null;
};
} & { " $fragmentName"?: "OAuth2Session_SessionFragment" };
@ -1226,6 +1231,55 @@ export type EndOAuth2SessionMutation = {
};
};
export type BrowserSession_DetailFragment = {
__typename?: "BrowserSession";
id: string;
createdAt: string;
finishedAt?: string | null;
userAgent?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
lastAuthentication?: {
__typename?: "Authentication";
id: string;
createdAt: string;
} | null;
user: { __typename?: "User"; id: string; username: string };
} & { " $fragmentName"?: "BrowserSession_DetailFragment" };
export type CompatSession_DetailFragment = {
__typename?: "CompatSession";
id: string;
createdAt: string;
deviceId: string;
finishedAt?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
ssoLogin?: {
__typename?: "CompatSsoLogin";
id: string;
redirectUri: string;
} | null;
} & { " $fragmentName"?: "CompatSession_DetailFragment" };
export type OAuth2Session_DetailFragment = {
__typename?: "Oauth2Session";
id: string;
scope: string;
createdAt: string;
finishedAt?: string | null;
lastActiveIp?: string | null;
lastActiveAt?: string | null;
client: {
__typename?: "Oauth2Client";
id: string;
clientId: string;
clientName?: string | null;
clientUri?: string | null;
logoUri?: string | null;
};
} & { " $fragmentName"?: "OAuth2Session_DetailFragment" };
export type SessionQueryQueryVariables = Exact<{
userId: Scalars["ID"]["input"];
deviceId: Scalars["String"]["input"];
@ -1236,12 +1290,12 @@ export type SessionQueryQuery = {
session?:
| ({ __typename: "CompatSession" } & {
" $fragmentRefs"?: {
CompatSession_SessionFragment: CompatSession_SessionFragment;
CompatSession_DetailFragment: CompatSession_DetailFragment;
};
})
| ({ __typename: "Oauth2Session" } & {
" $fragmentRefs"?: {
OAuth2Session_SessionFragment: OAuth2Session_SessionFragment;
OAuth2Session_DetailFragment: OAuth2Session_DetailFragment;
};
})
| null;
@ -1257,7 +1311,7 @@ export type UserEmail_EmailFragment = {
__typename?: "UserEmail";
id: string;
email: string;
confirmedAt?: any | null;
confirmedAt?: string | null;
} & { " $fragmentName"?: "UserEmail_EmailFragment" };
export type RemoveEmailMutationVariables = Exact<{
@ -1505,20 +1559,6 @@ export type ResendVerificationEmailMutation = {
};
};
export type BrowserSession_DetailFragment = {
__typename?: "BrowserSession";
id: string;
createdAt: any;
finishedAt?: any | null;
userAgent?: string | null;
lastAuthentication?: {
__typename?: "Authentication";
id: string;
createdAt: any;
} | null;
user: { __typename?: "User"; id: string; username: string };
} & { " $fragmentName"?: "BrowserSession_DetailFragment" };
export type BrowserSessionQueryQueryVariables = Exact<{
id: Scalars["ID"]["input"];
}>;
@ -1596,6 +1636,8 @@ export const BrowserSession_SessionFragmentDoc = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },
@ -1655,6 +1697,8 @@ export const CompatSession_SessionFragmentDoc = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "ssoLogin" },
@ -1688,6 +1732,126 @@ export const OAuth2Session_SessionFragmentDoc = {
{ kind: "Field", name: { kind: "Name", value: "scope" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "client" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<OAuth2Session_SessionFragment, unknown>;
export const BrowserSession_DetailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "BrowserSession_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "BrowserSession" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "username" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<BrowserSession_DetailFragment, unknown>;
export const CompatSession_DetailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "CompatSession_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "CompatSession" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "ssoLogin" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<CompatSession_DetailFragment, unknown>;
export const OAuth2Session_DetailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "OAuth2Session_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "Oauth2Session" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "scope" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "client" },
@ -1706,7 +1870,7 @@ export const OAuth2Session_SessionFragmentDoc = {
},
},
],
} as unknown as DocumentNode<OAuth2Session_SessionFragment, unknown>;
} as unknown as DocumentNode<OAuth2Session_DetailFragment, unknown>;
export const UnverifiedEmailAlertFragmentDoc = {
kind: "Document",
definitions: [
@ -1831,50 +1995,6 @@ export const UserEmail_VerifyEmailFragmentDoc = {
},
],
} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
export const BrowserSession_DetailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "BrowserSession_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "BrowserSession" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "username" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<BrowserSession_DetailFragment, unknown>;
export const CurrentViewerQueryDocument = {
kind: "Document",
definitions: [
@ -2063,6 +2183,8 @@ export const EndBrowserSessionDocument = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },
@ -2295,6 +2417,8 @@ export const BrowserSessionListDocument = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },
@ -2465,6 +2589,8 @@ export const EndOAuth2SessionDocument = {
{ kind: "Field", name: { kind: "Name", value: "scope" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "client" },
@ -2474,7 +2600,6 @@ export const EndOAuth2SessionDocument = {
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
],
},
@ -2551,11 +2676,11 @@ export const SessionQueryDocument = {
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "CompatSession_session" },
name: { kind: "Name", value: "CompatSession_detail" },
},
{
kind: "FragmentSpread",
name: { kind: "Name", value: "OAuth2Session_session" },
name: { kind: "Name", value: "OAuth2Session_detail" },
},
],
},
@ -2565,7 +2690,7 @@ export const SessionQueryDocument = {
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "CompatSession_session" },
name: { kind: "Name", value: "CompatSession_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "CompatSession" },
@ -2577,6 +2702,8 @@ export const SessionQueryDocument = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "ssoLogin" },
@ -2593,7 +2720,7 @@ export const SessionQueryDocument = {
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "OAuth2Session_session" },
name: { kind: "Name", value: "OAuth2Session_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "Oauth2Session" },
@ -2605,6 +2732,8 @@ export const SessionQueryDocument = {
{ kind: "Field", name: { kind: "Name", value: "scope" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "client" },
@ -3563,6 +3692,8 @@ export const AppSessionListDocument = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "ssoLogin" },
@ -3591,6 +3722,8 @@ export const AppSessionListDocument = {
{ kind: "Field", name: { kind: "Name", value: "scope" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "client" },
@ -3600,7 +3733,6 @@ export const AppSessionListDocument = {
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
],
},
@ -3907,6 +4039,8 @@ export const BrowserSessionQueryDocument = {
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
{
kind: "Field",
name: { kind: "Name", value: "lastAuthentication" },

View File

@ -24,23 +24,6 @@ import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDeta
import { graphql } from "../gql";
import { isErr, unwrapErr, unwrapOk } from "../result";
export const BROWSER_SESSION_DETAIL_FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_detail on BrowserSession {
id
createdAt
finishedAt
userAgent
lastAuthentication {
id
createdAt
}
user {
id
username
}
}
`);
const QUERY = graphql(/* GraphQL */ `
query BrowserSessionQuery($id: ID!) {
browserSession(id: $id) {

View File

@ -122,7 +122,7 @@ export default defineConfig((env) => ({
proxy: {
// Routes mostly extracted from crates/router/src/endpoints.rs
"^/(|graphql.*|assets.*|\\.well-known.*|oauth2.*|login.*|logout.*|register.*|reauth.*|add-email.*|verify-email.*|change-password.*|consent.*|_matrix.*|complete-compat-sso.*)$":
"https://auth-oidc.lab.element.dev",
"http://127.0.0.1:8080",
},
},