diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 72df9d5fe..1f5b4499d 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -10,6 +10,7 @@ "expand": "Expand", "save": "Save", "save_and_continue": "Save and continue", + "sign_out": "Sign out", "start_over": "Start over" }, "branding": { @@ -45,6 +46,10 @@ "change_disabled": "Password changes are disabled by the administrator.", "label": "Password" }, + "sign_out": { + "button": "Sign out of account", + "dialog": "Sign out of this account?" + }, "title": "Your account" }, "add_email_form": { @@ -69,8 +74,7 @@ }, "compat_session_detail": { "client_details_title": "Client info", - "name": "Name", - "session_details_title": "Session" + "name": "Name" }, "device_type_icon_label": { "mobile": "Mobile", @@ -83,7 +87,7 @@ }, "end_session_button": { "confirmation_modal_title": "Are you sure you want to end this session?", - "text": "Sign out" + "text": "Remove device" }, "error": { "hideDetails": "Hide details", @@ -233,6 +237,7 @@ "current": "Current", "device_id_label": "Device ID", "finished_label": "Finished", + "generic_browser_session": "Browser session", "ip_label": "IP Address", "last_active_label": "Last Active", "name_for_platform": "{{name}} for {{platform}}", diff --git a/frontend/src/components/Block/Block.module.css b/frontend/src/components/Block/Block.module.css deleted file mode 100644 index 5399077ae..000000000 --- a/frontend/src/components/Block/Block.module.css +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.block { - width: 100%; - color: var(--cpd-color-text-primary); - padding-bottom: var(--cpd-space-5x); - - &:last-child { - border-bottom: none; - } -} - -.title { - padding-bottom: var(--cpd-space-2x); - border-bottom: var(--cpd-border-width-2) solid var(--cpd-color-gray-400); - - /* Workaround compound design tokens heading style being broken */ - font-weight: var(--cpd-font-weight-semibold) !important; - font-size: var(--cpd-font-size-heading-sm) !important; -} diff --git a/frontend/src/components/Block/Block.stories.tsx b/frontend/src/components/Block/Block.stories.tsx deleted file mode 100644 index b0dd5e5ae..000000000 --- a/frontend/src/components/Block/Block.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { Meta, StoryObj } from "@storybook/react"; -import { Body, H1, H5 } from "@vector-im/compound-web"; - -import Block from "./Block"; - -const meta = { - title: "UI/Block", - component: Block, - tags: ["autodocs"], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Basic: Story = { - render: (args) => ( - -

Title

-
Subtitle
- - Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit - enim labore culpa sint ad nisi Lorem pariatur mollit ex esse - exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit - nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor - minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure - elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor - Lorem duis laboris cupidatat officia voluptate. Culpa proident - adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. - Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. - Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa - et culpa duis. - -
- ), -}; diff --git a/frontend/src/components/Block/Block.test.tsx b/frontend/src/components/Block/Block.test.tsx deleted file mode 100644 index 95d22f7db..000000000 --- a/frontend/src/components/Block/Block.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -// @vitest-environment happy-dom - -import { describe, expect, it } from "vitest"; - -import render from "../../test-utils/render"; -import Block from "./Block"; - -describe("Block", () => { - it("render ", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("render with children", () => { - const { asFragment } = render( - -

Title

-

Body

-
, - ); - expect(asFragment()).toMatchSnapshot(); - }); - - it("passes down the className prop", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders with highlight", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/frontend/src/components/Block/Block.tsx b/frontend/src/components/Block/Block.tsx deleted file mode 100644 index 0062da1b1..000000000 --- a/frontend/src/components/Block/Block.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2022-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { Heading } from "@vector-im/compound-web"; -import cx from "classnames"; -import type { ReactNode } from "react"; - -import styles from "./Block.module.css"; - -type Props = React.PropsWithChildren<{ - title?: ReactNode; - className?: string; - highlight?: boolean; -}>; - -const Block: React.FC = ({ children, className, highlight, title }) => { - return ( -
- {title && ( - - {title} - - )} - - {children} -
- ); -}; - -export default Block; diff --git a/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap b/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap deleted file mode 100644 index b48bcbd4c..000000000 --- a/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Block > passes down the className prop 1`] = ` - -
- -`; - -exports[`Block > render 1`] = ` - -
- -`; - -exports[`Block > render with children 1`] = ` - -
-

- Title -

-

- Body -

-
-
-`; - -exports[`Block > renders with highlight 1`] = ` - -
- -`; diff --git a/frontend/src/components/Block/index.ts b/frontend/src/components/Block/index.ts deleted file mode 100644 index 8cf98ec84..000000000 --- a/frontend/src/components/Block/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -export { default } from "./Block"; diff --git a/frontend/src/components/BlockList/BlockList.module.css b/frontend/src/components/BlockList/BlockList.module.css deleted file mode 100644 index e9fe11643..000000000 --- a/frontend/src/components/BlockList/BlockList.module.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.block-list { - display: flex; - flex-direction: column; - align-content: flex-start; - gap: var(--cpd-space-6x); -} diff --git a/frontend/src/components/BlockList/BlockList.stories.tsx b/frontend/src/components/BlockList/BlockList.stories.tsx deleted file mode 100644 index 8f965331d..000000000 --- a/frontend/src/components/BlockList/BlockList.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { Meta, StoryObj } from "@storybook/react"; -import { H2, Text } from "@vector-im/compound-web"; - -import Block from "../Block"; - -import BlockList from "./BlockList"; - -const meta = { - title: "UI/Block List", - component: BlockList, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Basic: Story = { - render: (args) => ( - - -

Block 1

- Body 1 -
- -

Block 2

- Body 2 -
-
- ), -}; diff --git a/frontend/src/components/BlockList/BlockList.test.tsx b/frontend/src/components/BlockList/BlockList.test.tsx deleted file mode 100644 index 0a470d5eb..000000000 --- a/frontend/src/components/BlockList/BlockList.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -// @vitest-environment happy-dom - -import { describe, expect, it } from "vitest"; -import render from "../../test-utils/render"; -import Block from "../Block"; -import BlockList from "./BlockList"; - -describe("BlockList", () => { - it("render an empty ", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("render with children", () => { - const { asFragment } = render( - - Block 1 - Block 2 - , - ); - expect(asFragment()).toMatchSnapshot(); - }); - - it("passes down the className prop", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/frontend/src/components/BlockList/BlockList.tsx b/frontend/src/components/BlockList/BlockList.tsx deleted file mode 100644 index cac68d945..000000000 --- a/frontend/src/components/BlockList/BlockList.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2022-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import cx from "classnames"; - -import styles from "./BlockList.module.css"; - -type Props = React.PropsWithChildren<{ - className?: string; -}>; - -const BlockList: React.FC = ({ className, children }) => { - return
{children}
; -}; - -export default BlockList; diff --git a/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap b/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap deleted file mode 100644 index 69d5a7a86..000000000 --- a/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`BlockList > passes down the className prop 1`] = ` - -
- -`; - -exports[`BlockList > render with children 1`] = ` - -
-
- Block 1 -
-
- Block 2 -
-
-
-`; - -exports[`BlockList > render an empty 1`] = ` - -
- -`; diff --git a/frontend/src/components/BlockList/index.ts b/frontend/src/components/BlockList/index.ts deleted file mode 100644 index 46df82ca5..000000000 --- a/frontend/src/components/BlockList/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -export { default } from "./BlockList"; diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index 45808eead..3b2e7a58a 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -7,15 +7,12 @@ import IconChrome from "@browser-logos/chrome/chrome_64x64.png?url"; import IconFirefox from "@browser-logos/firefox/firefox_64x64.png?url"; import IconSafari from "@browser-logos/safari/safari_64x64.png?url"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Badge } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; -import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../gql"; -import { graphqlRequest } from "../graphql"; import DateTime from "./DateTime"; -import EndSessionButton from "./Session/EndSessionButton"; +import EndBrowserSessionButton from "./Session/EndBrowserSessionButton"; import LastActive from "./Session/LastActive"; import * as Card from "./SessionCard"; @@ -24,62 +21,17 @@ const FRAGMENT = graphql(/* GraphQL */ ` id createdAt finishedAt + ...EndBrowserSessionButton_session userAgent { - raw + deviceType name os model - deviceType } - lastActiveIp lastActiveAt - lastAuthentication { - id - createdAt - } - } -`); - -const END_SESSION_MUTATION = graphql(/* GraphQL */ ` - mutation EndBrowserSession($id: ID!) { - endBrowserSession(input: { browserSessionId: $id }) { - status - browserSession { - id - ...BrowserSession_session - } - } } `); -export const useEndBrowserSession = ( - sessionId: string, - isCurrent: boolean, -): (() => Promise) => { - const queryClient = useQueryClient(); - const endSession = useMutation({ - mutationFn: (id: string) => - graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); - queryClient.invalidateQueries({ queryKey: ["browserSessionList"] }); - queryClient.invalidateQueries({ - queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id], - }); - - if (isCurrent) { - window.location.reload(); - } - }, - }); - - const onSessionEnd = useCallback(async (): Promise => { - await endSession.mutateAsync(sessionId); - }, [endSession.mutateAsync, sessionId]); - - return onSessionEnd; -}; - export const browserLogoUri = (browser?: string): string | undefined => { const lcBrowser = browser?.toLowerCase(); @@ -105,8 +57,6 @@ const BrowserSession: React.FC = ({ session, isCurrent }) => { const data = useFragment(FRAGMENT, session); const { t } = useTranslation(); - const onSessionEnd = useEndBrowserSession(data.id, isCurrent); - const deviceType = data.userAgent?.deviceType ?? "UNKNOWN"; let deviceName: string | null = null; @@ -175,14 +125,7 @@ const BrowserSession: React.FC = ({ session, isCurrent }) => { {!data.finishedAt && ( - - - - - {clientName && } - - - + )} diff --git a/frontend/src/components/Client/OAuth2ClientDetail.module.css b/frontend/src/components/Client/OAuth2ClientDetail.module.css deleted file mode 100644 index 52af4eeb4..000000000 --- a/frontend/src/components/Client/OAuth2ClientDetail.module.css +++ /dev/null @@ -1,14 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.header { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: var(--cpd-space-2x); -} diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx index dac8c51ca..1c8d019c2 100644 --- a/frontend/src/components/Client/OAuth2ClientDetail.tsx +++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only @@ -9,12 +9,9 @@ import { useTranslation } from "react-i18next"; import { type FragmentType, useFragment } from "../../gql"; import { graphql } from "../../gql/gql"; -import BlockList from "../BlockList/BlockList"; import ExternalLink from "../ExternalLink/ExternalLink"; import ClientAvatar from "../Session/ClientAvatar"; -import SessionDetails from "../SessionDetail/SessionDetails"; - -import styles from "./OAuth2ClientDetail.module.css"; +import * as Info from "../SessionDetail/SessionInfo"; export const OAUTH2_CLIENT_FRAGMENT = graphql(/* GraphQL */ ` fragment OAuth2Client_detail on Oauth2Client { @@ -47,21 +44,9 @@ const OAuth2ClientDetail: React.FC = ({ client }) => { const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client); const { t } = useTranslation(); - const details = [ - { label: t("frontend.oauth2_client_detail.name"), value: data.clientName }, - { - label: t("frontend.oauth2_client_detail.terms"), - value: data.tosUri && , - }, - { - label: t("frontend.oauth2_client_detail.policy"), - value: data.policyUri && , - }, - ].filter(({ value }) => !!value); - return ( - -
+
+
= ({ client }) => { />

{data.clientName}

- - + + + {t("frontend.oauth2_client_detail.details_title")} + + + {data.clientName && ( + + + {t("frontend.oauth2_client_detail.name")} + + {data.clientName} + + )} + {data.tosUri && ( + + + {t("frontend.oauth2_client_detail.terms")} + + + + + + )} + {data.policyUri && ( + + + {t("frontend.oauth2_client_detail.policy")} + + + + + + )} + + +
); }; diff --git a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap index 833e5ae68..214083888 100644 --- a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap +++ b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap @@ -3,10 +3,10 @@ exports[` > renders client details 1`] = `

> renders client details 1`] = ` Test Client

-

Client info +

-
-
Name

Test Client

-
-
+
  • Terms of service
    - - client.org/tos - -
  • -
    + client.org/tos + +

    + +
  • Policy
    - - client.org/policy - -
  • -
    -
    + + client.org/policy + +

    + + +
    `; diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx index 16b732d4d..2ea3fdd60 100644 --- a/frontend/src/components/CompatSession.tsx +++ b/frontend/src/components/CompatSession.tsx @@ -1,17 +1,16 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../gql"; -import { graphqlRequest } from "../graphql"; +import simplifyUrl from "../utils/simplifyUrl"; import { browserLogoUri } from "./BrowserSession"; import DateTime from "./DateTime"; -import EndSessionButton from "./Session/EndSessionButton"; +import EndCompatSessionButton from "./Session/EndCompatSessionButton"; import LastActive from "./Session/LastActive"; import * as Card from "./SessionCard"; @@ -23,6 +22,7 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + ...EndCompatSessionButton_session userAgent { name os @@ -36,59 +36,11 @@ export const FRAGMENT = graphql(/* GraphQL */ ` } `); -export const END_SESSION_MUTATION = graphql(/* GraphQL */ ` - mutation EndCompatSession($id: ID!) { - endCompatSession(input: { compatSessionId: $id }) { - status - compatSession { - id - } - } - } -`); - -export const simplifyUrl = (url: string): string => { - let parsed: URL; - try { - parsed = new URL(url); - } catch (_e) { - // Not a valid URL, return the original - return url; - } - - // Clear out the search params and hash - parsed.search = ""; - parsed.hash = ""; - - if (parsed.protocol === "https:") { - return parsed.hostname; - } - - // Return the simplified URL - return parsed.toString(); -}; - const CompatSession: React.FC<{ session: FragmentType; }> = ({ session }) => { const { t } = useTranslation(); const data = useFragment(FRAGMENT, session); - const queryClient = useQueryClient(); - const endSession = useMutation({ - mutationFn: (id: string) => - graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); - queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); - queryClient.invalidateQueries({ - queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id], - }); - }, - }); - - const onSessionEnd = async (): Promise => { - await endSession.mutateAsync(data.id); - }; const clientName = data.ssoLogin?.redirectUri ? simplifyUrl(data.ssoLogin.redirectUri) @@ -146,14 +98,7 @@ const CompatSession: React.FC<{ {!data.finishedAt && ( - - - - - {clientName && } - - - + )} diff --git a/frontend/src/components/GenericError.tsx b/frontend/src/components/GenericError.tsx index 815477761..000a190d1 100644 --- a/frontend/src/components/GenericError.tsx +++ b/frontend/src/components/GenericError.tsx @@ -8,8 +8,6 @@ import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error" import { Button } from "@vector-im/compound-web"; import { useState } from "react"; import { Translation } from "react-i18next"; - -import BlockList from "./BlockList"; import styles from "./GenericError.module.css"; import PageHeading from "./PageHeading"; @@ -21,7 +19,7 @@ const GenericError: React.FC<{ error: unknown; dontSuspend?: boolean }> = ({ return ( {(t) => ( - +
    = ({ {String(error)} )} - +
    )}
    ); diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index 5652a84fc..cc92f26c6 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -1,18 +1,10 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2022-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../gql"; import type { DeviceType, Oauth2ApplicationType } from "../gql/graphql"; -import { graphqlRequest } from "../graphql"; import { getDeviceIdFromScope } from "../utils/deviceIdFromScope"; import DateTime from "./DateTime"; -import EndSessionButton from "./Session/EndSessionButton"; +import EndOAuth2SessionButton from "./Session/EndOAuth2SessionButton"; import LastActive from "./Session/LastActive"; import * as Card from "./SessionCard"; @@ -25,6 +17,8 @@ export const FRAGMENT = graphql(/* GraphQL */ ` lastActiveIp lastActiveAt + ...EndOAuth2SessionButton_session + userAgent { name model @@ -42,17 +36,6 @@ export const FRAGMENT = graphql(/* GraphQL */ ` } `); -export const END_SESSION_MUTATION = graphql(/* GraphQL */ ` - mutation EndOAuth2Session($id: ID!) { - endOauth2Session(input: { oauth2SessionId: $id }) { - status - oauth2Session { - id - } - } - } -`); - const getDeviceTypeFromClientAppType = ( appType?: Oauth2ApplicationType | null, ): DeviceType => { @@ -72,22 +55,6 @@ type Props = { const OAuth2Session: React.FC = ({ session }) => { const { t } = useTranslation(); const data = useFragment(FRAGMENT, session); - const queryClient = useQueryClient(); - const endSession = useMutation({ - mutationFn: (id: string) => - graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); - queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); - queryClient.invalidateQueries({ - queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id], - }); - }, - }); - - const onSessionEnd = async (): Promise => { - await endSession.mutateAsync(data.id); - }; const deviceId = getDeviceIdFromScope(data.scope); @@ -149,17 +116,7 @@ const OAuth2Session: React.FC = ({ session }) => { {!data.finishedAt && ( - - - - - - - - + )} diff --git a/frontend/src/components/Session/EndBrowserSessionButton.tsx b/frontend/src/components/Session/EndBrowserSessionButton.tsx new file mode 100644 index 000000000..f04a9298c --- /dev/null +++ b/frontend/src/components/Session/EndBrowserSessionButton.tsx @@ -0,0 +1,128 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import { + type UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlRequest } from "../../graphql"; +import * as Card from "../SessionCard"; +import EndSessionButton from "./EndSessionButton"; + +const FRAGMENT = graphql(/* GraphQL */ ` + fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType + } + } +`); + +const END_SESSION_MUTATION = graphql(/* GraphQL */ ` + mutation EndBrowserSession($id: ID!) { + endBrowserSession(input: { browserSessionId: $id }) { + status + browserSession { + id + } + } + } +`); + +export const useEndBrowserSession = ( + sessionId: string, + isCurrent: boolean, +): UseMutationResult => { + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: () => + graphqlRequest({ + query: END_SESSION_MUTATION, + variables: { id: sessionId }, + }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["browserSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id], + }); + + if (isCurrent) { + window.location.reload(); + } + }, + }); + + return endSession; +}; + +type Props = { + session: FragmentType; + size: "sm" | "lg"; +}; + +const EndBrowserSessionButton: React.FC = ({ session, size }) => { + const { t } = useTranslation(); + const data = useFragment(FRAGMENT, session); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: () => + graphqlRequest({ + query: END_SESSION_MUTATION, + variables: { id: data.id }, + }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id], + }); + }, + }); + + const deviceType = data.userAgent?.deviceType ?? "UNKNOWN"; + + let deviceName: string | null = null; + let clientName: string | null = null; + + // If we have a model, use that as the device name, and the browser (+ OS) as the client name + if (data.userAgent?.model) { + deviceName = data.userAgent.model; + if (data.userAgent?.name) { + if (data.userAgent?.os) { + clientName = t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }); + } else { + clientName = data.userAgent.name; + } + } + } else { + // Else use the browser as the device name + deviceName = data.userAgent?.name ?? t("frontend.session.unknown_browser"); + // and if we have an OS, use that as the client name + clientName = data.userAgent?.os ?? null; + } + + return ( + + + + + {clientName && } + + + + ); +}; + +export default EndBrowserSessionButton; diff --git a/frontend/src/components/Session/EndCompatSessionButton.tsx b/frontend/src/components/Session/EndCompatSessionButton.tsx new file mode 100644 index 000000000..68f67b91b --- /dev/null +++ b/frontend/src/components/Session/EndCompatSessionButton.tsx @@ -0,0 +1,89 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlRequest } from "../../graphql"; +import simplifyUrl from "../../utils/simplifyUrl"; +import * as Card from "../SessionCard"; +import EndSessionButton from "./EndSessionButton"; + +const FRAGMENT = graphql(/* GraphQL */ ` + fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } + } +`); + +const END_SESSION_MUTATION = graphql(/* GraphQL */ ` + mutation EndCompatSession($id: ID!) { + endCompatSession(input: { compatSessionId: $id }) { + status + compatSession { + id + } + } + } +`); + +type Props = { + session: FragmentType; + size: "sm" | "lg"; +}; + +const EndCompatSessionButton: React.FC = ({ session, size }) => { + const { t } = useTranslation(); + const data = useFragment(FRAGMENT, session); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: () => + graphqlRequest({ + query: END_SESSION_MUTATION, + variables: { id: data.id }, + }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id], + }); + }, + }); + + const clientName = data.ssoLogin?.redirectUri + ? simplifyUrl(data.ssoLogin.redirectUri) + : undefined; + + const deviceType = data.userAgent?.deviceType ?? "UNKNOWN"; + + const deviceName = + data.userAgent?.model ?? + (data.userAgent?.name + ? data.userAgent?.os + ? t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }) + : data.userAgent.name + : t("frontend.session.unknown_device")); + + return ( + + + + + {clientName && } + + + + ); +}; + +export default EndCompatSessionButton; diff --git a/frontend/src/components/Session/EndOAuth2SessionButton.tsx b/frontend/src/components/Session/EndOAuth2SessionButton.tsx new file mode 100644 index 000000000..245ddad12 --- /dev/null +++ b/frontend/src/components/Session/EndOAuth2SessionButton.tsx @@ -0,0 +1,115 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { type FragmentType, graphql, useFragment } from "../../gql"; +import type { DeviceType, Oauth2ApplicationType } from "../../gql/graphql"; +import { graphqlRequest } from "../../graphql"; +import * as Card from "../SessionCard"; +import EndSessionButton from "./EndSessionButton"; + +const FRAGMENT = graphql(/* GraphQL */ ` + fragment EndOAuth2SessionButton_session on Oauth2Session { + id + + userAgent { + name + model + os + deviceType + } + + client { + clientId + clientName + applicationType + logoUri + } + } +`); + +const END_SESSION_MUTATION = graphql(/* GraphQL */ ` + mutation EndOAuth2Session($id: ID!) { + endOauth2Session(input: { oauth2SessionId: $id }) { + status + oauth2Session { + id + } + } + } +`); + +const getDeviceTypeFromClientAppType = ( + appType?: Oauth2ApplicationType | null, +): DeviceType => { + if (appType === "WEB") { + return "PC"; + } + if (appType === "NATIVE") { + return "MOBILE"; + } + return "UNKNOWN"; +}; + +type Props = { + session: FragmentType; + size: "sm" | "lg"; +}; + +const EndOAuth2SessionButton: React.FC = ({ session, size }) => { + const { t } = useTranslation(); + const data = useFragment(FRAGMENT, session); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: () => + graphqlRequest({ + query: END_SESSION_MUTATION, + variables: { id: data.id }, + }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id], + }); + }, + }); + + const deviceType = + (data.userAgent?.deviceType === "UNKNOWN" + ? null + : data.userAgent?.deviceType) ?? + getDeviceTypeFromClientAppType(data.client.applicationType); + + const clientName = data.client.clientName || data.client.clientId; + + const deviceName = + data.userAgent?.model ?? + (data.userAgent?.name + ? data.userAgent?.os + ? t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }) + : data.userAgent.name + : t("frontend.session.unknown_device")); + + return ( + + + + + + + + + ); +}; + +export default EndOAuth2SessionButton; diff --git a/frontend/src/components/Session/EndSessionButton.stories.tsx b/frontend/src/components/Session/EndSessionButton.stories.tsx deleted file mode 100644 index 838f51a27..000000000 --- a/frontend/src/components/Session/EndSessionButton.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { action } from "@storybook/addon-actions"; -import type { Meta, StoryObj } from "@storybook/react"; - -import EndSessionButton from "./EndSessionButton"; - -const endSession = action("end-session"); - -const meta = { - title: "UI/Session/End Session Button", - component: EndSessionButton, - tags: ["autodocs"], - args: { - endSession: async (): Promise => { - await new Promise((resolve) => setTimeout(resolve, 300)); - endSession(); - }, - }, - argTypes: { - children: { control: "text" }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Basic: Story = {}; - -export const WithChildren: Story = { - args: { - children: - "Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.", - }, -}; diff --git a/frontend/src/components/Session/EndSessionButton.tsx b/frontend/src/components/Session/EndSessionButton.tsx index 54a54853c..4b311066f 100644 --- a/frontend/src/components/Session/EndSessionButton.tsx +++ b/frontend/src/components/Session/EndSessionButton.tsx @@ -1,14 +1,14 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -import IconSignOut from "@vector-im/compound-design-tokens/assets/web/icons/sign-out"; +import type { UseMutationResult } from "@tanstack/react-query"; +import IconDelete from "@vector-im/compound-design-tokens/assets/web/icons/delete"; import { Button } from "@vector-im/compound-web"; import { useState } from "react"; import { useTranslation } from "react-i18next"; - import * as Dialog from "../Dialog"; import LoadingSpinner from "../LoadingSpinner/LoadingSpinner"; @@ -17,25 +17,17 @@ import LoadingSpinner from "../LoadingSpinner/LoadingSpinner"; * Handles loading state while endSession is in progress */ const EndSessionButton: React.FC< - React.PropsWithChildren<{ endSession: () => Promise }> -> = ({ children, endSession }) => { - const [inProgress, setInProgress] = useState(false); + React.PropsWithChildren<{ + mutation: UseMutationResult; + size: "sm" | "lg"; + }> +> = ({ children, mutation, size }) => { const [open, setOpen] = useState(false); const { t } = useTranslation(); - const onConfirm = async ( - e: React.MouseEvent, - ): Promise => { + const onConfirm = (e: React.MouseEvent): void => { e.preventDefault(); - - setInProgress(true); - try { - await endSession(); - setOpen(false); - } catch (error) { - console.error("Failed to end session", error); - } - setInProgress(false); + mutation.mutate(void 0, { onSuccess: () => setOpen(false) }); }; return ( @@ -43,7 +35,7 @@ const EndSessionButton: React.FC< open={open} onOpenChange={setOpen} trigger={ - } @@ -59,10 +51,10 @@ const EndSessionButton: React.FC< kind="primary" destructive onClick={onConfirm} - disabled={inProgress} - Icon={inProgress ? undefined : IconSignOut} + disabled={mutation.isPending} + Icon={mutation.isPending ? undefined : IconDelete} > - {inProgress && } + {mutation.isPending && } {t("frontend.end_session_button.text")} diff --git a/frontend/src/components/SessionCard/SessionCard.module.css b/frontend/src/components/SessionCard/SessionCard.module.css index b36c3819d..ce3f02052 100644 --- a/frontend/src/components/SessionCard/SessionCard.module.css +++ b/frontend/src/components/SessionCard/SessionCard.module.css @@ -111,6 +111,10 @@ flex-wrap: wrap; gap: var(--cpd-space-4x) var(--cpd-space-10x); + & > * { + min-width: 0; + } + & .key { font: var(--cpd-font-body-sm-regular); letter-spacing: var(--cpd-font-letter-spacing-body-sm); @@ -121,6 +125,8 @@ font: var(--cpd-font-body-md-regular); letter-spacing: var(--cpd-font-letter-spacing-body-md); color: var(--cpd-color-text-primary); + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css b/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css deleted file mode 100644 index d14155405..000000000 --- a/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css +++ /dev/null @@ -1,10 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.current-badge { - align-self: flex-start; -} diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx index f5009e829..2fe2db3f9 100644 --- a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only @@ -7,22 +7,19 @@ import { Badge } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; - import { type FragmentType, graphql, useFragment } from "../../gql"; -import BlockList from "../BlockList/BlockList"; -import { useEndBrowserSession } from "../BrowserSession"; import DateTime from "../DateTime"; -import EndSessionButton from "../Session/EndSessionButton"; - -import styles from "./BrowserSessionDetail.module.css"; -import SessionDetails from "./SessionDetails"; +import EndBrowserSessionButton from "../Session/EndBrowserSessionButton"; +import LastActive from "../Session/LastActive"; import SessionHeader from "./SessionHeader"; +import * as Info from "./SessionInfo"; const FRAGMENT = graphql(/* GraphQL */ ` fragment BrowserSession_detail on BrowserSession { id createdAt finishedAt + ...EndBrowserSessionButton_session userAgent { name model @@ -50,51 +47,79 @@ const BrowserSessionDetail: React.FC = ({ session, isCurrent }) => { const data = useFragment(FRAGMENT, session); const { t } = useTranslation(); - const onSessionEnd = useEndBrowserSession(data.id, isCurrent); - - let sessionName = "Browser session"; + let sessionName = t("frontend.session.generic_browser_session"); if (data.userAgent) { if (data.userAgent.model && data.userAgent.name) { - sessionName = `${data.userAgent.name} on ${data.userAgent.model}`; + sessionName = t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.model, + }); } else if (data.userAgent.name && data.userAgent.os) { - sessionName = `${data.userAgent.name} on ${data.userAgent.os}`; + sessionName = t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }); } else if (data.userAgent.name) { sessionName = data.userAgent.name; } } - const finishedAt = data.finishedAt - ? [ - { - label: t("frontend.session.finished_label"), - value: , - }, - ] - : []; - - const sessionDetails = [...finishedAt]; - return ( - +
    {isCurrent && ( - + {t("frontend.browser_session_details.current_badge")} )} {sessionName} - - {!data.finishedAt && } - + + + {t("frontend.session.title")} + + + {data.lastActiveAt && ( + + + {t("frontend.session.last_active_label")} + + + + + + )} + + + + {t("frontend.session.signed_in_label")} + + + + + + + {data.finishedAt && ( + + + {t("frontend.session.finished_label")} + + + + + + )} + + {data.lastActiveIp && ( + + {t("frontend.session.ip_label")} + + {data.lastActiveIp} + + + )} + + + {!data.finishedAt && } +
    ); }; diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx index c9bbc28d0..30fb72418 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx @@ -38,7 +38,7 @@ describe("", () => { expect(container).toMatchSnapshot(); expect(queryByText("Finished")).toBeFalsy(); - expect(getByText("Sign out")).toBeTruthy(); + expect(getByText("Remove device")).toBeTruthy(); }); it("renders a compatability session without an ssoLogin", () => { @@ -56,7 +56,7 @@ describe("", () => { expect(container).toMatchSnapshot(); expect(queryByText("Finished")).toBeFalsy(); - expect(getByText("Sign out")).toBeTruthy(); + expect(getByText("Remove device")).toBeTruthy(); }); it("renders a finished compatability session details", () => { @@ -74,6 +74,6 @@ describe("", () => { expect(container).toMatchSnapshot(); expect(getByText("Finished")).toBeTruthy(); - expect(queryByText("Sign out")).toBeFalsy(); + expect(queryByText("Remove device")).toBeFalsy(); }); }); diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 097c130d3..144e7ef37 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -1,21 +1,19 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { VisualList } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../../gql"; -import { graphqlRequest } from "../../graphql"; -import BlockList from "../BlockList/BlockList"; -import { END_SESSION_MUTATION, simplifyUrl } from "../CompatSession"; +import simplifyUrl from "../../utils/simplifyUrl"; import DateTime from "../DateTime"; -import ExternalLink from "../ExternalLink/ExternalLink"; -import EndSessionButton from "../Session/EndSessionButton"; -import SessionDetails from "./SessionDetails"; +import EndCompatSessionButton from "../Session/EndCompatSessionButton"; +import LastActive from "../Session/LastActive"; import SessionHeader from "./SessionHeader"; +import * as Info from "./SessionInfo"; export const FRAGMENT = graphql(/* GraphQL */ ` fragment CompatSession_detail on CompatSession { @@ -25,11 +23,15 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + + ...EndCompatSessionButton_session + userAgent { name os model } + ssoLogin { id redirectUri @@ -43,74 +45,111 @@ type Props = { const CompatSessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); - const queryClient = useQueryClient(); - const endSession = useMutation({ - mutationFn: (id: string) => - graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); - queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); - queryClient.invalidateQueries({ - queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id], - }); - }, - }); const { t } = useTranslation(); - const onSessionEnd = async (): Promise => { - await endSession.mutateAsync(data.id); - }; - - const finishedAt = data.finishedAt - ? [ - { - label: t("frontend.session.finished_label"), - value: , - }, - ] - : []; - - const sessionDetails = [...finishedAt]; - - const clientDetails: { label: string; value: string | React.ReactElement }[] = - []; - - if (data.ssoLogin?.redirectUri) { - clientDetails.push({ - label: t("frontend.compat_session_detail.name"), - value: data.userAgent?.name ?? simplifyUrl(data.ssoLogin.redirectUri), - }); - clientDetails.push({ - label: t("frontend.session.uri_label"), - value: ( - - {data.ssoLogin?.redirectUri} - - ), - }); - } + const deviceName = + data.userAgent?.model ?? + (data.userAgent?.name + ? data.userAgent?.os + ? t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }) + : data.userAgent.name + : t("frontend.session.unknown_device")); + + const clientName = data.ssoLogin?.redirectUri + ? simplifyUrl(data.ssoLogin.redirectUri) + : data.deviceId || data.id; return ( - - {data.deviceId || data.id} - - {clientDetails.length > 0 ? ( - - ) : null} - {!data.finishedAt && } - +
    + + {clientName}: {deviceName} + + + + {t("frontend.session.title")} + + + {data.lastActiveAt && ( + + + {t("frontend.session.last_active_label")} + + + + + + )} + + + + {t("frontend.session.signed_in_label")} + + + + + + + {data.finishedAt && ( + + + {t("frontend.session.finished_label")} + + + + + + )} + + + + {t("frontend.session.device_id_label")} + + {data.deviceId} + + + {data.lastActiveIp && ( + + {t("frontend.session.ip_label")} + + {data.lastActiveIp} + + + )} + + + + {t("frontend.session.scopes_label")} + + + + + + + + + + + {t("frontend.compat_session_detail.client_details_title")} + + + + + {t("frontend.compat_session_detail.name")} + + {deviceName} + + + {t("frontend.session.uri_label")} + {data.ssoLogin?.redirectUri} + + + + + {!data.finishedAt && } +
    ); }; diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx index b0c30c7d0..7f33da2bb 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx @@ -44,7 +44,7 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); expect(queryByText("Finished")).toBeFalsy(); - expect(getByText("Sign out")).toBeTruthy(); + expect(getByText("Remove device")).toBeTruthy(); }); it("renders a finished session details", () => { @@ -62,6 +62,6 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); expect(getByText("Finished")).toBeTruthy(); - expect(queryByText("Sign out")).toBeFalsy(); + expect(queryByText("Remove device")).toBeFalsy(); }); }); diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 58d32eae3..656067cd0 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -1,23 +1,19 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../../gql"; -import { graphqlRequest } from "../../graphql"; import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope"; -import BlockList from "../BlockList/BlockList"; import DateTime from "../DateTime"; -import { Link } from "../Link"; -import { END_SESSION_MUTATION } from "../OAuth2Session"; import ClientAvatar from "../Session/ClientAvatar"; -import EndSessionButton from "../Session/EndSessionButton"; -import SessionDetails from "./SessionDetails"; +import EndOAuth2SessionButton from "../Session/EndOAuth2SessionButton"; +import LastActive from "../Session/LastActive"; import SessionHeader from "./SessionHeader"; +import * as Info from "./SessionInfo"; export const FRAGMENT = graphql(/* GraphQL */ ` fragment OAuth2Session_detail on Oauth2Session { @@ -27,6 +23,15 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + + ...EndOAuth2SessionButton_session + + userAgent { + name + model + os + } + client { id clientId @@ -43,90 +48,129 @@ type Props = { const OAuth2SessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); - const queryClient = useQueryClient(); - const endSession = useMutation({ - mutationFn: (id: string) => - graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); - queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); - queryClient.invalidateQueries({ - queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id], - }); - }, - }); - const { t } = useTranslation(); - const onSessionEnd = async (): Promise => { - await endSession.mutateAsync(data.id); - }; - const deviceId = getDeviceIdFromScope(data.scope); + const clientName = data.client.clientName || data.client.clientId; - const finishedAt = data.finishedAt - ? [ - { - label: t("frontend.session.finished_label"), - value: , - }, - ] - : []; - - const sessionDetails = [...finishedAt]; - - const clientTitle = ( - - {t("frontend.oauth2_session_detail.client_title")} - - ); - const clientDetails = [ - { - label: t("frontend.oauth2_session_detail.client_details_name"), - value: ( - <> - - {data.client.clientName} - - ), - }, - { - label: t("frontend.session.client_id_label"), - value: {data.client.clientId}, - }, - { - label: t("frontend.session.uri_label"), - value: ( - - {data.client.clientUri} - - ), - }, - ]; + const deviceName = + data.userAgent?.model ?? + (data.userAgent?.name + ? data.userAgent?.os + ? t("frontend.session.name_for_platform", { + name: data.userAgent.name, + platform: data.userAgent.os, + }) + : data.userAgent.name + : t("frontend.session.unknown_device")); return ( - - {deviceId || data.id} - - - {!data.finishedAt && } - +
    + + {clientName}: {deviceName} + + + + {t("frontend.session.title")} + + + {data.lastActiveAt && ( + + + {t("frontend.session.last_active_label")} + + + + + + )} + + + + {t("frontend.session.signed_in_label")} + + + + + + + {data.finishedAt && ( + + + {t("frontend.session.finished_label")} + + + + + + )} + + + + {t("frontend.session.device_id_label")} + + {deviceId} + + + {data.lastActiveIp && ( + + {t("frontend.session.ip_label")} + + {data.lastActiveIp} + + + )} + + + + {t("frontend.session.scopes_label")} + + + + + + + {t("frontend.oauth2_session_detail.client_title")} + + + + + {t("frontend.oauth2_session_detail.client_details_name")} + + + + {data.client.clientName} + + + + + {t("frontend.session.client_id_label")} + + + {data.client.clientId} + + + + {t("frontend.session.uri_label")} + + + {data.client.clientUri} + + + + + + + {!data.finishedAt && } +
    ); }; diff --git a/frontend/src/components/SessionDetail/SessionDetails.module.css b/frontend/src/components/SessionDetail/SessionDetails.module.css deleted file mode 100644 index 5b3275a0d..000000000 --- a/frontend/src/components/SessionDetail/SessionDetails.module.css +++ /dev/null @@ -1,33 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.wrapper { - display: flex; - flex-wrap: wrap; - gap: var(--cpd-space-4x); - margin-bottom: var(--cpd-space-4x); - margin-top: var(--cpd-space-8x); -} - -.wrapper h5 { - color: var(--cpd-color-text-secondary); -} - -.wrapper .datum { - width: max-content; -} - -.datum { - flex-grow: 1; - max-width: 100%; -} - -.datum-value { - font-size: var(--cpd-font-size-body-md); - text-overflow: ellipsis; - overflow: hidden; -} diff --git a/frontend/src/components/SessionDetail/SessionDetails.tsx b/frontend/src/components/SessionDetail/SessionDetails.tsx deleted file mode 100644 index 67430360b..000000000 --- a/frontend/src/components/SessionDetail/SessionDetails.tsx +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2022-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import IconChat from "@vector-im/compound-design-tokens/assets/web/icons/chat"; -import IconComputer from "@vector-im/compound-design-tokens/assets/web/icons/computer"; -import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; -import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info"; -import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send"; -import IconUserProfile from "@vector-im/compound-design-tokens/assets/web/icons/user-profile"; -import { Text } from "@vector-im/compound-web"; -import type { ReactNode } from "react"; -import { useTranslation } from "react-i18next"; - -import Block from "../Block/Block"; -import DateTime from "../DateTime"; -import LastActive from "../Session/LastActive"; -import { VisualList, VisualListItem } from "../VisualList/VisualList"; - -import styles from "./SessionDetails.module.css"; - -type Detail = { label: string; value: ReactNode }; -type Props = { - title: string | ReactNode; - lastActive?: Date; - signedIn?: Date; - deviceId?: string | null; - ipAddress?: string; - scopes?: string[]; - details?: Detail[]; -}; - -const Scope: React.FC<{ scope: string }> = ({ scope }) => { - const { t } = useTranslation(); - // Filter out "urn:matrix:org.matrix.msc2967.client:device:" - if (scope.startsWith("urn:matrix:org.matrix.msc2967.client:device:")) { - return null; - } - - // Needs to be manually kept in sync with /templates/components/scope.html - const scopeMap: Record = { - openid: [[0, IconUserProfile, t("mas.scope.view_profile")]], - "urn:mas:graphql:*": [ - [1, IconInfo, t("mas.scope.edit_profile")], - [2, IconComputer, t("mas.scope.manage_sessions")], - ], - "urn:matrix:org.matrix.msc2967.client:api:*": [ - [3, IconChat, t("mas.scope.view_messages")], - [4, IconSend, t("mas.scope.send_messages")], - ], - "urn:synapse:admin:*": [[5, IconError, t("mas.scope.synapse_admin")]], - "urn:mas:admin": [[6, IconError, t("mas.scope.mas_admin")]], - } as const; - - const mappedScopes: [number | string, typeof IconInfo, string][] = scopeMap[ - scope - ] ?? [[scope, IconInfo, scope]]; - - return ( - <> - {mappedScopes.map(([key, Icon, text]) => ( - - ))} - - ); -}; - -const Datum: React.FC<{ label: string; value: ReactNode }> = ({ - label, - value, -}) => { - return ( -
    - - {label} - - {typeof value === "string" ? ( - - {value} - - ) : ( - value - )} -
    - ); -}; - -const SessionDetails: React.FC = ({ - title, - lastActive, - signedIn, - deviceId, - ipAddress, - details, - scopes, -}) => { - const { t } = useTranslation(); - - return ( - -
    - {lastActive && ( - - } - /> - )} - {signedIn && ( - - } - /> - )} - {deviceId && ( - - )} - {ipAddress && ( - {ipAddress}} - /> - )} - {details?.map(({ label, value }) => ( - - ))} -
    - - {scopes?.length && ( - - {scopes.map((scope) => ( - - ))} - - } - /> - )} -
    - ); -}; - -export default SessionDetails; diff --git a/frontend/src/components/SessionDetail/SessionInfo.tsx b/frontend/src/components/SessionDetail/SessionInfo.tsx new file mode 100644 index 000000000..c6632f48f --- /dev/null +++ b/frontend/src/components/SessionDetail/SessionInfo.tsx @@ -0,0 +1,183 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import IconChat from "@vector-im/compound-design-tokens/assets/web/icons/chat"; +import IconComputer from "@vector-im/compound-design-tokens/assets/web/icons/computer"; +import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info"; +import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send"; +import IconUserProfile from "@vector-im/compound-design-tokens/assets/web/icons/user-profile"; +import { + Heading, + Separator, + Text, + VisualList, + VisualListItem, +} from "@vector-im/compound-web"; +import cx from "classnames"; +import type * as React from "react"; +import { useTranslation } from "react-i18next"; + +export const ScopeViewProfile: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.view_profile")} + + ); +}; + +const ScopeEditProfile: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.edit_profile")} + + ); +}; + +const ScopeManageSessions: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.manage_sessions")} + + ); +}; + +export const ScopeViewMessages: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.view_messages")} + + ); +}; + +export const ScopeSendMessages: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.send_messages")} + + ); +}; + +const ScopeSynapseAdmin: React.FC = () => { + const { t } = useTranslation(); + return ( + + {t("mas.scope.synapse_admin")} + + ); +}; + +const ScopeMasAdmin: React.FC = () => { + const { t } = useTranslation(); + return ( + {t("mas.scope.mas_admin")} + ); +}; + +const ScopeOther: React.FC<{ scope: string }> = ({ scope }) => { + return {scope}; +}; + +const Scope: React.FC<{ scope: string }> = ({ scope }) => { + // Filter out "urn:matrix:org.matrix.msc2967.client:device:" + if (scope.startsWith("urn:matrix:org.matrix.msc2967.client:device:")) { + return null; + } + + switch (scope) { + case "openid": + return ; + case "urn:mas:graphql:*": + return ( + <> + + + + ); + case "urn:matrix:org.matrix.msc2967.client:api:*": + return ( + <> + + + + ); + case "urn:synapse:admin:*": + return ; + case "urn:mas:admin": + return ; + default: + return ; + } +}; + +export const ScopeList: React.FC<{ scope: string }> = ({ scope }) => { + const scopes = scope.split(" "); + return ( + + {scopes.map((scope) => ( + + ))} + + ); +}; + +export const Data: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( +
  • {children}
  • +); + +export const DataLabel: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( + + {children} + +); + +export const DataValue: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( + + {children} + +); + +export const DataList: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( +
      + {children} +
    +); + +export const DataSection: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( +
    {children}
    +); + +export const DataSectionHeader: React.FC< + React.PropsWithChildren<{ className?: string }> +> = ({ children, className }) => ( + + {children} + + +); diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap index 850626e72..c4ccb5f05 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap @@ -3,7 +3,7 @@ exports[` > renders a compatability session details 1`] = `
    > renders a compatability session details 1`] = `

    - abcd1234 + element.io + : + Unknown device

    -

    - Session + Device details +

    -
    -
    Last Active
    - - Inactive for 90+ days - -
    -
    + Inactive for 90+ days + +

    + +
  • Signed in
    - -
  • -
    + Thu, 29 Jun 2023, 03:35 + +

    + +
  • Device ID

    abcd1234

    -
  • -
    +
  • IP Address
    - - 1.2.3.4 - -
  • -
    -
    + + 1.2.3.4 + +

    + + +
  • Scopes
    • -

      - See your profile info and contact details -

      + See your profile info and contact details
    • -

      - View your existing messages and data -

      + View your existing messages and data
    • -

      - Send new messages on your behalf -

      + Send new messages on your behalf
    -
  • -
    -
    + +

    Client info +

    -
    -
    Name

    - element.io + Unknown device

    -
    - -
    -
    +

    + + +
    @@ -264,7 +272,7 @@ exports[` > renders a compatability session details 1`] = ` exports[` > renders a compatability session without an ssoLogin 1`] = `
    > renders a compatability session without an ssoL class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121" > abcd1234 + : + Unknown device
    -

    - Session + Device details +

    -
    -
    Last Active
    - - Inactive for 90+ days - -
    -
    + Inactive for 90+ days + +

    + +
  • Signed in
    - -
  • -
    + Thu, 29 Jun 2023, 03:35 + +

    + +
  • Device ID

    abcd1234

    -
  • -
    +
  • IP Address
    - - 1.2.3.4 - -
  • -
    -
    + + 1.2.3.4 + +

    + + +
  • Scopes
    • -

      - See your profile info and contact details -

      + See your profile info and contact details
    • -

      - View your existing messages and data -

      + View your existing messages and data
    • -

      - Send new messages on your behalf -

      + Send new messages on your behalf
    -
  • -
    + + +
    +

    + Client info +

    +
      +
    • +
      + Name +
      +

      + Unknown device +

      +
    • +
    • +
      + Uri +
      +

      +

    • +
    +
    @@ -479,7 +539,7 @@ exports[` > renders a compatability session without an ssoL exports[` > renders a finished compatability session details 1`] = `
    > renders a finished compatability session detail

    - abcd1234 + element.io + : + Unknown device

    -

    - Session + Device details +

    -
    -
    Last Active
    - - Inactive for 90+ days - -
    -
    + Inactive for 90+ days + +

    + +
  • Signed in
    - -
  • -
    + Thu, 29 Jun 2023, 03:35 + +

    + +
  • - Device ID + Finished

    - abcd1234 +

    -
  • -
    +
  • - IP Address + Device ID
    - - 1.2.3.4 - -
  • -
    + +
  • - Finished + IP Address
    - -
  • -
    -
    + + 1.2.3.4 + +

    + + +
  • Scopes
    • -

      - See your profile info and contact details -

      + See your profile info and contact details
    • -

      - View your existing messages and data -

      + View your existing messages and data
    • -

      - Send new messages on your behalf -

      + Send new messages on your behalf
    -
  • -
    -
    + +

    Client info +

    -
    -
    Name

    - element.io + Unknown device

    -
    - -
    -
    +

    + + +
    `; diff --git a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap index 1b4a32865..3535ea0de 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap @@ -3,7 +3,7 @@ exports[` > renders a finished session details 1`] = `
    > renders a finished session details 1`] = `

    - abcd1234 + Element: Unknown device

    -

    Device details +

    -
    -
    Last Active
    - - Inactive for 90+ days - -
    -
    + Inactive for 90+ days + +

    + +
  • Signed in
    - -
  • -
    + Thu, 29 Jun 2023, 03:35 + +

    + +
  • - Device ID + Finished

    - abcd1234 +

    -
  • -
    +
  • - IP Address + Device ID
    - - 1.2.3.4 - -
  • -
    + +
  • - Finished + IP Address
    - -
  • -
    -
    + + 1.2.3.4 + +

    + + +
  • Scopes
    • -

      - See your profile info and contact details -

      + See your profile info and contact details
    • -

      - View your existing messages and data -

      + View your existing messages and data
    • -

      - Send new messages on your behalf -

      + Send new messages on your behalf
    -
  • -
    -
    + +

    - - Client info - + Client info +

    -
    -
    Name
    - Element -
    -
    + Element +

    + +
  • Client ID
    - - test-client-id - -
  • -
    + + test-client-id + +

    + +
  • Uri
    - - https://element.io - -
  • -
    -
    +

    + + https://element.io + +

    + + +
    `; @@ -265,7 +284,7 @@ exports[` > renders a finished session details 1`] = ` exports[` > renders session details 1`] = `
    > renders session details 1`] = `

    - abcd1234 + Element: Unknown device

    -

    Device details +

    -
    -
    Last Active
    - - Inactive for 90+ days - -
    -
    + Inactive for 90+ days + +

    + +
  • Signed in
    - -
  • -
    + Thu, 29 Jun 2023, 03:35 + +

    + +
  • Device ID

    abcd1234

    -
  • -
    +
  • IP Address
    - - 1.2.3.4 - -
  • -
    -
    + + 1.2.3.4 + +

    + + +
  • Scopes
    • -

      - See your profile info and contact details -

      + See your profile info and contact details
    • -

      - View your existing messages and data -

      + View your existing messages and data
    • -

      - Send new messages on your behalf -

      + Send new messages on your behalf
    -
  • -
    -
    + +

    - - Client info - + Client info +

    -
    -
    Name
    - Element -
    -
    + Element +

    + +
  • Client ID
    - - test-client-id - -
  • -
    + + test-client-id + +

    + +
  • Uri
    - - https://element.io - -
  • -
    -
    +

    + + https://element.io + +

    + + +
    diff --git a/frontend/src/components/VisualList/VisualList.module.css b/frontend/src/components/VisualList/VisualList.module.css deleted file mode 100644 index 7f60c94bf..000000000 --- a/frontend/src/components/VisualList/VisualList.module.css +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. - */ - -.list { - display: flex; - flex-direction: column; - gap: var(--cpd-space-scale); - border-radius: var(--cpd-space-5x); - overflow: hidden; -} - -.item { - background: var(--cpd-color-bg-action-secondary-hovered); - padding: var(--cpd-space-3x) var(--cpd-space-5x); - display: flex; - align-items: center; - gap: var(--cpd-space-3x); -} - -.item svg { - inline-size: var(--cpd-space-6x); - block-size: var(--cpd-space-6x); - flex-shrink: 0; -} diff --git a/frontend/src/components/VisualList/VisualList.tsx b/frontend/src/components/VisualList/VisualList.tsx deleted file mode 100644 index 7eda8818b..000000000 --- a/frontend/src/components/VisualList/VisualList.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { Text } from "@vector-im/compound-web"; -import type { - FC, - ForwardRefExoticComponent, - ReactNode, - RefAttributes, - SVGProps, -} from "react"; - -import styles from "./VisualList.module.css"; - -type Props = { - children: ReactNode; -}; - -export const VisualListItem: FC<{ - Icon: ForwardRefExoticComponent< - Omit, "ref" | "children"> & - RefAttributes - >; - iconColor?: string; - label: string; -}> = ({ Icon, iconColor, label }) => { - return ( -
  • - - {label} -
  • - ); -}; - -export const VisualList: React.FC = ({ children }) => { - return
      {children}
    ; -}; diff --git a/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap index 8b80f55e2..0421e8bd3 100644 --- a/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap +++ b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap @@ -182,10 +182,10 @@ exports[` > renders an active session 1`] = ` xmlns="http://www.w3.org/2000/svg" > - Sign out + Remove device
    diff --git a/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap index 14c5b3da6..1c27b6b89 100644 --- a/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap +++ b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap @@ -176,10 +176,10 @@ exports[` > renders an active session 1`] = ` xmlns="http://www.w3.org/2000/svg" > - Sign out + Remove device
    diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 9f144ba22..0254e2a1b 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -16,19 +16,22 @@ import * as types from './graphql'; */ type Documents = { "\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": typeof types.PasswordChange_SiteConfigFragmentDoc, - "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": typeof 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": typeof types.EndBrowserSessionDocument, + "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": typeof types.BrowserSession_SessionFragmentDoc, "\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": typeof types.OAuth2Client_DetailFragmentDoc, - "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc, - "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": typeof types.EndCompatSessionDocument, + "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument, - "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc, - "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument, + "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc, "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc, - "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc, - "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof 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": typeof types.OAuth2Session_DetailFragmentDoc, + "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc, + "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument, + "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.EndCompatSessionButton_SessionFragmentDoc, + "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": typeof types.EndCompatSessionDocument, + "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.EndOAuth2SessionButton_SessionFragmentDoc, + "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument, + "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc, + "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc, + "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc, "\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmail_SiteConfigFragmentDoc, "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument, @@ -39,12 +42,11 @@ type Documents = { "\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": typeof types.UserEmailListDocument, "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc, "\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc, - "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument, - "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": typeof types.SessionDetailDocument, + "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument, "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument, "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument, "\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument, - "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": typeof types.CurrentUserGreetingDocument, + "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": typeof types.CurrentUserGreetingDocument, "\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": typeof types.OAuth2ClientDocument, "\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.CurrentViewerDocument, "\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.DeviceRedirectDocument, @@ -59,22 +61,26 @@ type Documents = { "\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc, "\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": typeof types.PasswordRecoveryDocument, "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": typeof types.AllowCrossSigningResetDocument, + "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": typeof types.SessionDetailDocument, }; const documents: Documents = { "\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": types.PasswordChange_SiteConfigFragmentDoc, - "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\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, + "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": types.BrowserSession_SessionFragmentDoc, "\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 lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\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 }\n }\n }\n": types.EndCompatSessionDocument, + "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument, - "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\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 }\n }\n }\n": types.EndOAuth2SessionDocument, + "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc, "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc, - "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\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 userAgent {\n name\n os\n model\n }\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 fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc, + "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument, + "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.EndCompatSessionButton_SessionFragmentDoc, + "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": types.EndCompatSessionDocument, + "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.EndOAuth2SessionButton_SessionFragmentDoc, + "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument, + "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\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\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\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\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc, "\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc, "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, @@ -85,12 +91,11 @@ const documents: Documents = { "\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument, "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc, "\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc, - "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument, - "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument, + "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument, "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument, "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument, "\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument, - "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument, + "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument, "\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument, "\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerDocument, "\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectDocument, @@ -105,6 +110,7 @@ const documents: Documents = { "\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": types.RecoverPassword_SiteConfigFragmentDoc, "\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": types.PasswordRecoveryDocument, "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument, + "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument, }; /** @@ -114,55 +120,67 @@ export function graphql(source: "\n fragment PasswordChange_siteConfig on SiteC /** * 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 raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"): typeof import('./graphql').BrowserSession_SessionFragmentDoc; +export function graphql(source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n"): typeof import('./graphql').BrowserSession_SessionFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n"): typeof import('./graphql').EndBrowserSessionDocument; +export function graphql(source: "\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n"): typeof import('./graphql').OAuth2Client_DetailFragmentDoc; /** * 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 OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n"): typeof import('./graphql').OAuth2Client_DetailFragmentDoc; +export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc; /** * 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 lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc; +export function graphql(source: "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n"): typeof import('./graphql').Footer_SiteConfigFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndCompatSessionDocument; +export function graphql(source: "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n"): typeof import('./graphql').FooterDocument; /** * 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 Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n"): typeof import('./graphql').Footer_SiteConfigFragmentDoc; +export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc; /** * 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 Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n"): typeof import('./graphql').FooterDocument; +export function graphql(source: "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').PasswordCreationDoubleInput_SiteConfigFragmentDoc; /** * 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 lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc; +export function graphql(source: "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n"): typeof import('./graphql').EndBrowserSessionButton_SessionFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionDocument; +export function graphql(source: "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndBrowserSessionDocument; /** * 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 PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').PasswordCreationDoubleInput_SiteConfigFragmentDoc; +export function graphql(source: "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').EndCompatSessionButton_SessionFragmentDoc; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndCompatSessionDocument; +/** + * 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 EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionButton_SessionFragmentDoc; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionDocument; /** * 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 name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"): typeof import('./graphql').BrowserSession_DetailFragmentDoc; +export function graphql(source: "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"): typeof import('./graphql').BrowserSession_DetailFragmentDoc; /** * 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 userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc; +export function graphql(source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc; /** * 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 import('./graphql').OAuth2Session_DetailFragmentDoc; +export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -206,11 +224,7 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us /** * 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 UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument; -/** - * 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 SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n"): typeof import('./graphql').SessionDetailDocument; +export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -226,7 +240,7 @@ export function graphql(source: "\n query AppSessionsList(\n $before: String /** * 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 CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument; +export function graphql(source: "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -283,6 +297,10 @@ export function graphql(source: "\n query PasswordRecovery($ticket: String!) {\ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').AllowCrossSigningResetDocument; +/** + * 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 SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n"): typeof import('./graphql').SessionDetailDocument; export function graphql(source: string) { diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index cf32c2302..6564a67da 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1565,40 +1565,54 @@ export type ViewerSession = Anonymous | BrowserSession | Oauth2Session; export type PasswordChange_SiteConfigFragment = { __typename?: 'SiteConfig', passwordChangeAllowed: boolean } & { ' $fragmentName'?: 'PasswordChange_SiteConfigFragment' }; -export type BrowserSession_SessionFragment = { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', raw: string, name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null } & { ' $fragmentName'?: 'BrowserSession_SessionFragment' }; +export type BrowserSession_SessionFragment = ( + { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', deviceType: DeviceType, name?: string | null, os?: string | null, model?: string | null } | null } + & { ' $fragmentRefs'?: { 'EndBrowserSessionButton_SessionFragment': EndBrowserSessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'BrowserSession_SessionFragment' }; -export type EndBrowserSessionMutationVariables = Exact<{ - id: Scalars['ID']['input']; -}>; +export type OAuth2Client_DetailFragment = { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null, tosUri?: string | null, policyUri?: string | null, redirectUris: Array } & { ' $fragmentName'?: 'OAuth2Client_DetailFragment' }; +export type CompatSession_SessionFragment = ( + { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } + & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'CompatSession_SessionFragment' }; -export type EndBrowserSessionMutation = { __typename?: 'Mutation', endBrowserSession: { __typename?: 'EndBrowserSessionPayload', status: EndBrowserSessionStatus, browserSession?: ( - { __typename?: 'BrowserSession', id: string } - & { ' $fragmentRefs'?: { 'BrowserSession_SessionFragment': BrowserSession_SessionFragment } } - ) | null } }; +export type Footer_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, imprint?: string | null, tosUri?: string | null, policyUri?: string | null } & { ' $fragmentName'?: 'Footer_SiteConfigFragment' }; -export type OAuth2Client_DetailFragment = { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null, tosUri?: string | null, policyUri?: string | null, redirectUris: Array } & { ' $fragmentName'?: 'OAuth2Client_DetailFragment' }; +export type FooterQueryVariables = Exact<{ [key: string]: never; }>; -export type CompatSession_SessionFragment = { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'CompatSession_SessionFragment' }; -export type EndCompatSessionMutationVariables = Exact<{ +export type FooterQuery = { __typename?: 'Query', siteConfig: ( + { __typename?: 'SiteConfig', id: string } + & { ' $fragmentRefs'?: { 'Footer_SiteConfigFragment': Footer_SiteConfigFragment } } + ) }; + +export type OAuth2Session_SessionFragment = ( + { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } + & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' }; + +export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' }; + +export type EndBrowserSessionButton_SessionFragment = { __typename?: 'BrowserSession', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null } & { ' $fragmentName'?: 'EndBrowserSessionButton_SessionFragment' }; + +export type EndBrowserSessionMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string } | null } }; +export type EndBrowserSessionMutation = { __typename?: 'Mutation', endBrowserSession: { __typename?: 'EndBrowserSessionPayload', status: EndBrowserSessionStatus, browserSession?: { __typename?: 'BrowserSession', id: string } | null } }; -export type Footer_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, imprint?: string | null, tosUri?: string | null, policyUri?: string | null } & { ' $fragmentName'?: 'Footer_SiteConfigFragment' }; +export type EndCompatSessionButton_SessionFragment = { __typename?: 'CompatSession', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'EndCompatSessionButton_SessionFragment' }; -export type FooterQueryVariables = Exact<{ [key: string]: never; }>; +export type EndCompatSessionMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; -export type FooterQuery = { __typename?: 'Query', siteConfig: ( - { __typename?: 'SiteConfig', id: string } - & { ' $fragmentRefs'?: { 'Footer_SiteConfigFragment': Footer_SiteConfigFragment } } - ) }; +export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string } | null } }; -export type OAuth2Session_SessionFragment = { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' }; +export type EndOAuth2SessionButton_SessionFragment = { __typename?: 'Oauth2Session', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } & { ' $fragmentName'?: 'EndOAuth2SessionButton_SessionFragment' }; export type EndOAuth2SessionMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -1607,13 +1621,20 @@ export type EndOAuth2SessionMutationVariables = Exact<{ export type EndOAuth2SessionMutation = { __typename?: 'Mutation', endOauth2Session: { __typename?: 'EndOAuth2SessionPayload', status: EndOAuth2SessionStatus, oauth2Session?: { __typename?: 'Oauth2Session', id: string } | null } }; -export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' }; +export type BrowserSession_DetailFragment = ( + { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null, user: { __typename?: 'User', id: string, username: string } } + & { ' $fragmentRefs'?: { 'EndBrowserSessionButton_SessionFragment': EndBrowserSessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'BrowserSession_DetailFragment' }; -export type BrowserSession_DetailFragment = { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | 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 | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } + & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'CompatSession_DetailFragment' }; -export type CompatSession_DetailFragment = { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | 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 OAuth2Session_DetailFragment = ( + { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } } + & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } } +) & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' }; export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_EmailFragment' }; @@ -1666,27 +1687,11 @@ export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: st export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>; -export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } }, siteConfig: ( +export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: ( { __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean } & { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } } ) }; -export type SessionDetailQueryVariables = Exact<{ - id: Scalars['ID']['input']; -}>; - - -export type SessionDetailQuery = { __typename?: 'Query', viewerSession: { __typename?: 'Anonymous', id: string } | { __typename?: 'BrowserSession', id: string } | { __typename?: 'Oauth2Session', id: string }, node?: { __typename: 'Anonymous', id: string } | { __typename: 'Authentication', id: string } | ( - { __typename: 'BrowserSession', id: string } - & { ' $fragmentRefs'?: { 'BrowserSession_DetailFragment': BrowserSession_DetailFragment } } - ) | ( - { __typename: 'CompatSession', id: string } - & { ' $fragmentRefs'?: { 'CompatSession_DetailFragment': CompatSession_DetailFragment } } - ) | { __typename: 'CompatSsoLogin', id: string } | { __typename: 'Oauth2Client', id: string } | ( - { __typename: 'Oauth2Session', id: string } - & { ' $fragmentRefs'?: { 'OAuth2Session_DetailFragment': OAuth2Session_DetailFragment } } - ) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserEmailAuthentication', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null }; - export type BrowserSessionListQueryVariables = Exact<{ first?: InputMaybe; after?: InputMaybe; @@ -1729,10 +1734,10 @@ export type AppSessionsListQuery = { __typename?: 'Query', viewer: { __typename: export type CurrentUserGreetingQueryVariables = Exact<{ [key: string]: never; }>; -export type CurrentUserGreetingQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: ( - { __typename?: 'User' } - & { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } } - ) } | { __typename: 'Oauth2Session' }, siteConfig: ( +export type CurrentUserGreetingQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | ( + { __typename: 'User' } + & { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } } + ), siteConfig: ( { __typename?: 'SiteConfig' } & { ' $fragmentRefs'?: { 'UserGreeting_SiteConfigFragment': UserGreeting_SiteConfigFragment } } ) }; @@ -1842,6 +1847,22 @@ export type AllowCrossSigningResetMutationVariables = Exact<{ export type AllowCrossSigningResetMutation = { __typename?: 'Mutation', allowUserCrossSigningReset: { __typename?: 'AllowUserCrossSigningResetPayload', user?: { __typename?: 'User', id: string } | null } }; +export type SessionDetailQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type SessionDetailQuery = { __typename?: 'Query', viewerSession: { __typename?: 'Anonymous', id: string } | { __typename?: 'BrowserSession', id: string } | { __typename?: 'Oauth2Session', id: string }, node?: { __typename: 'Anonymous', id: string } | { __typename: 'Authentication', id: string } | ( + { __typename: 'BrowserSession', id: string } + & { ' $fragmentRefs'?: { 'BrowserSession_DetailFragment': BrowserSession_DetailFragment } } + ) | ( + { __typename: 'CompatSession', id: string } + & { ' $fragmentRefs'?: { 'CompatSession_DetailFragment': CompatSession_DetailFragment } } + ) | { __typename: 'CompatSsoLogin', id: string } | { __typename: 'Oauth2Client', id: string } | ( + { __typename: 'Oauth2Session', id: string } + & { ' $fragmentRefs'?: { 'OAuth2Session_DetailFragment': OAuth2Session_DetailFragment } } + ) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserEmailAuthentication', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null }; + export class TypedDocumentString extends String implements DocumentTypeDecoration @@ -1861,26 +1882,40 @@ export const PasswordChange_SiteConfigFragmentDoc = new TypedDocumentString(` passwordChangeAllowed } `, {"fragmentName":"PasswordChange_siteConfig"}) as unknown as TypedDocumentString; +export const EndBrowserSessionButton_SessionFragmentDoc = new TypedDocumentString(` + fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType + } +} + `, {"fragmentName":"EndBrowserSessionButton_session"}) as unknown as TypedDocumentString; export const BrowserSession_SessionFragmentDoc = new TypedDocumentString(` fragment BrowserSession_session on BrowserSession { id createdAt finishedAt + ...EndBrowserSessionButton_session userAgent { - raw + deviceType name os model - deviceType } - lastActiveIp lastActiveAt - lastAuthentication { - id - createdAt - } } - `, {"fragmentName":"BrowserSession_session"}) as unknown as TypedDocumentString; + fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType + } +}`, {"fragmentName":"BrowserSession_session"}) as unknown as TypedDocumentString; export const OAuth2Client_DetailFragmentDoc = new TypedDocumentString(` fragment OAuth2Client_detail on Oauth2Client { id @@ -1893,6 +1928,21 @@ export const OAuth2Client_DetailFragmentDoc = new TypedDocumentString(` redirectUris } `, {"fragmentName":"OAuth2Client_detail"}) as unknown as TypedDocumentString; +export const EndCompatSessionButton_SessionFragmentDoc = new TypedDocumentString(` + fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } +} + `, {"fragmentName":"EndCompatSessionButton_session"}) as unknown as TypedDocumentString; export const CompatSession_SessionFragmentDoc = new TypedDocumentString(` fragment CompatSession_session on CompatSession { id @@ -1901,6 +1951,7 @@ export const CompatSession_SessionFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + ...EndCompatSessionButton_session userAgent { name os @@ -1912,7 +1963,19 @@ export const CompatSession_SessionFragmentDoc = new TypedDocumentString(` redirectUri } } - `, {"fragmentName":"CompatSession_session"}) as unknown as TypedDocumentString; + fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } +}`, {"fragmentName":"CompatSession_session"}) as unknown as TypedDocumentString; export const Footer_SiteConfigFragmentDoc = new TypedDocumentString(` fragment Footer_siteConfig on SiteConfig { id @@ -1921,6 +1984,23 @@ export const Footer_SiteConfigFragmentDoc = new TypedDocumentString(` policyUri } `, {"fragmentName":"Footer_siteConfig"}) as unknown as TypedDocumentString; +export const EndOAuth2SessionButton_SessionFragmentDoc = new TypedDocumentString(` + fragment EndOAuth2SessionButton_session on Oauth2Session { + id + userAgent { + name + model + os + deviceType + } + client { + clientId + clientName + applicationType + logoUri + } +} + `, {"fragmentName":"EndOAuth2SessionButton_session"}) as unknown as TypedDocumentString; export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(` fragment OAuth2Session_session on Oauth2Session { id @@ -1929,6 +2009,7 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + ...EndOAuth2SessionButton_session userAgent { name model @@ -1943,12 +2024,27 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(` logoUri } } - `, {"fragmentName":"OAuth2Session_session"}) as unknown as TypedDocumentString; + fragment EndOAuth2SessionButton_session on Oauth2Session { + id + userAgent { + name + model + os + deviceType + } + client { + clientId + clientName + applicationType + logoUri + } +}`, {"fragmentName":"OAuth2Session_session"}) as unknown as TypedDocumentString; export const BrowserSession_DetailFragmentDoc = new TypedDocumentString(` fragment BrowserSession_detail on BrowserSession { id createdAt finishedAt + ...EndBrowserSessionButton_session userAgent { name model @@ -1965,7 +2061,15 @@ export const BrowserSession_DetailFragmentDoc = new TypedDocumentString(` username } } - `, {"fragmentName":"BrowserSession_detail"}) as unknown as TypedDocumentString; + fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType + } +}`, {"fragmentName":"BrowserSession_detail"}) as unknown as TypedDocumentString; export const CompatSession_DetailFragmentDoc = new TypedDocumentString(` fragment CompatSession_detail on CompatSession { id @@ -1974,6 +2078,7 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + ...EndCompatSessionButton_session userAgent { name os @@ -1984,7 +2089,19 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(` redirectUri } } - `, {"fragmentName":"CompatSession_detail"}) as unknown as TypedDocumentString; + fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } +}`, {"fragmentName":"CompatSession_detail"}) as unknown as TypedDocumentString; export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(` fragment OAuth2Session_detail on Oauth2Session { id @@ -1993,6 +2110,12 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + ...EndOAuth2SessionButton_session + userAgent { + name + model + os + } client { id clientId @@ -2001,7 +2124,21 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(` logoUri } } - `, {"fragmentName":"OAuth2Session_detail"}) as unknown as TypedDocumentString; + fragment EndOAuth2SessionButton_session on Oauth2Session { + id + userAgent { + name + model + os + deviceType + } + client { + clientId + clientName + applicationType + logoUri + } +}`, {"fragmentName":"OAuth2Session_detail"}) as unknown as TypedDocumentString; export const UserEmail_EmailFragmentDoc = new TypedDocumentString(` fragment UserEmail_email on UserEmail { id @@ -2060,34 +2197,29 @@ export const RecoverPassword_SiteConfigFragmentDoc = new TypedDocumentString(` id minimumPasswordComplexity }`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString; +export const FooterDocument = new TypedDocumentString(` + query Footer { + siteConfig { + id + ...Footer_siteConfig + } +} + fragment Footer_siteConfig on SiteConfig { + id + imprint + tosUri + policyUri +}`) as unknown as TypedDocumentString; export const EndBrowserSessionDocument = new TypedDocumentString(` mutation EndBrowserSession($id: ID!) { endBrowserSession(input: {browserSessionId: $id}) { status browserSession { id - ...BrowserSession_session } } } - fragment BrowserSession_session on BrowserSession { - id - createdAt - finishedAt - userAgent { - raw - name - os - model - deviceType - } - lastActiveIp - lastActiveAt - lastAuthentication { - id - createdAt - } -}`) as unknown as TypedDocumentString; + `) as unknown as TypedDocumentString; export const EndCompatSessionDocument = new TypedDocumentString(` mutation EndCompatSession($id: ID!) { endCompatSession(input: {compatSessionId: $id}) { @@ -2098,19 +2230,6 @@ export const EndCompatSessionDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; -export const FooterDocument = new TypedDocumentString(` - query Footer { - siteConfig { - id - ...Footer_siteConfig - } -} - fragment Footer_siteConfig on SiteConfig { - id - imprint - tosUri - policyUri -}`) as unknown as TypedDocumentString; export const EndOAuth2SessionDocument = new TypedDocumentString(` mutation EndOAuth2Session($id: ID!) { endOauth2Session(input: {oauth2SessionId: $id}) { @@ -2178,11 +2297,14 @@ export const UserEmailListDocument = new TypedDocumentString(` }`) as unknown as TypedDocumentString; export const UserProfileDocument = new TypedDocumentString(` query UserProfile { - viewer { + viewerSession { __typename - ... on User { - emails(first: 0) { - totalCount + ... on BrowserSession { + id + user { + emails(first: 0) { + totalCount + } } } } @@ -2191,85 +2313,18 @@ export const UserProfileDocument = new TypedDocumentString(` passwordLoginEnabled ...UserEmailList_siteConfig ...UserEmail_siteConfig - ...PasswordChange_siteConfig - } -} - fragment PasswordChange_siteConfig on SiteConfig { - passwordChangeAllowed -} -fragment UserEmail_siteConfig on SiteConfig { - emailChangeAllowed -} -fragment UserEmailList_siteConfig on SiteConfig { - emailChangeAllowed -}`) as unknown as TypedDocumentString; -export const SessionDetailDocument = new TypedDocumentString(` - query SessionDetail($id: ID!) { - viewerSession { - ... on Node { - id - } - } - node(id: $id) { - __typename - id - ...CompatSession_detail - ...OAuth2Session_detail - ...BrowserSession_detail - } -} - fragment BrowserSession_detail on BrowserSession { - id - createdAt - finishedAt - userAgent { - name - model - os - } - lastActiveIp - lastActiveAt - lastAuthentication { - id - createdAt - } - user { - id - username - } -} -fragment CompatSession_detail on CompatSession { - id - createdAt - deviceId - finishedAt - lastActiveIp - lastActiveAt - userAgent { - name - os - model - } - ssoLogin { - id - redirectUri + ...PasswordChange_siteConfig } } -fragment OAuth2Session_detail on Oauth2Session { - id - scope - createdAt - finishedAt - lastActiveIp - lastActiveAt - client { - id - clientId - clientName - clientUri - logoUri - } -}`) as unknown as TypedDocumentString; + fragment PasswordChange_siteConfig on SiteConfig { + passwordChangeAllowed +} +fragment UserEmail_siteConfig on SiteConfig { + emailChangeAllowed +} +fragment UserEmailList_siteConfig on SiteConfig { + emailChangeAllowed +}`) as unknown as TypedDocumentString; export const BrowserSessionListDocument = new TypedDocumentString(` query BrowserSessionList($first: Int, $after: String, $last: Int, $before: String, $lastActive: DateFilter) { viewerSession { @@ -2309,18 +2364,22 @@ export const BrowserSessionListDocument = new TypedDocumentString(` id createdAt finishedAt + ...EndBrowserSessionButton_session userAgent { - raw + deviceType name os model - deviceType } - lastActiveIp lastActiveAt - lastAuthentication { - id - createdAt +} +fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType } }`) as unknown as TypedDocumentString; export const SessionsOverviewDocument = new TypedDocumentString(` @@ -2379,6 +2438,7 @@ export const AppSessionsListDocument = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + ...EndCompatSessionButton_session userAgent { name os @@ -2397,6 +2457,7 @@ fragment OAuth2Session_session on Oauth2Session { finishedAt lastActiveIp lastActiveAt + ...EndOAuth2SessionButton_session userAgent { name model @@ -2410,16 +2471,41 @@ fragment OAuth2Session_session on Oauth2Session { applicationType logoUri } +} +fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } +} +fragment EndOAuth2SessionButton_session on Oauth2Session { + id + userAgent { + name + model + os + deviceType + } + client { + clientId + clientName + applicationType + logoUri + } }`) as unknown as TypedDocumentString; export const CurrentUserGreetingDocument = new TypedDocumentString(` query CurrentUserGreeting { - viewerSession { + viewer { __typename - ... on BrowserSession { - id - user { - ...UserGreeting_user - } + ... on User { + ...UserGreeting_user } } siteConfig { @@ -2565,25 +2651,136 @@ export const AllowCrossSigningResetDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const SessionDetailDocument = new TypedDocumentString(` + query SessionDetail($id: ID!) { + viewerSession { + ... on Node { + id + } + } + node(id: $id) { + __typename + id + ...CompatSession_detail + ...OAuth2Session_detail + ...BrowserSession_detail + } +} + fragment EndBrowserSessionButton_session on BrowserSession { + id + userAgent { + name + os + model + deviceType + } +} +fragment EndCompatSessionButton_session on CompatSession { + id + userAgent { + name + os + model + deviceType + } + ssoLogin { + id + redirectUri + } +} +fragment EndOAuth2SessionButton_session on Oauth2Session { + id + userAgent { + name + model + os + deviceType + } + client { + clientId + clientName + applicationType + logoUri + } +} +fragment BrowserSession_detail on BrowserSession { + id + createdAt + finishedAt + ...EndBrowserSessionButton_session + userAgent { + name + model + os + } + lastActiveIp + lastActiveAt + lastAuthentication { + id + createdAt + } + user { + id + username + } +} +fragment CompatSession_detail on CompatSession { + id + createdAt + deviceId + finishedAt + lastActiveIp + lastActiveAt + ...EndCompatSessionButton_session + userAgent { + name + os + model + } + ssoLogin { + id + redirectUri + } +} +fragment OAuth2Session_detail on Oauth2Session { + id + scope + createdAt + finishedAt + lastActiveIp + lastActiveAt + ...EndOAuth2SessionButton_session + userAgent { + name + model + os + } + client { + id + clientId + clientName + clientUri + logoUri + } +}`) as unknown as TypedDocumentString; /** * @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions)) * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options)) * @see https://mswjs.io/docs/basics/response-resolver * @example - * mockEndBrowserSessionMutation( + * mockFooterQuery( * ({ query, variables }) => { - * const { id } = variables; * return HttpResponse.json({ - * data: { endBrowserSession } + * data: { siteConfig } * }) * }, * requestOptions * ) */ -export const mockEndBrowserSessionMutation = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => - graphql.mutation( - 'EndBrowserSession', +export const mockFooterQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => + graphql.query( + 'Footer', resolver, options ) @@ -2593,19 +2790,19 @@ export const mockEndBrowserSessionMutation = (resolver: GraphQLResponseResolver< * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options)) * @see https://mswjs.io/docs/basics/response-resolver * @example - * mockEndCompatSessionMutation( + * mockEndBrowserSessionMutation( * ({ query, variables }) => { * const { id } = variables; * return HttpResponse.json({ - * data: { endCompatSession } + * data: { endBrowserSession } * }) * }, * requestOptions * ) */ -export const mockEndCompatSessionMutation = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => - graphql.mutation( - 'EndCompatSession', +export const mockEndBrowserSessionMutation = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => + graphql.mutation( + 'EndBrowserSession', resolver, options ) @@ -2615,18 +2812,19 @@ export const mockEndCompatSessionMutation = (resolver: GraphQLResponseResolver { + * const { id } = variables; * return HttpResponse.json({ - * data: { siteConfig } + * data: { endCompatSession } * }) * }, * requestOptions * ) */ -export const mockFooterQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => - graphql.query( - 'Footer', +export const mockEndCompatSessionMutation = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => + graphql.mutation( + 'EndCompatSession', resolver, options ) @@ -2749,7 +2947,7 @@ export const mockUserEmailListQuery = (resolver: GraphQLResponseResolver { * return HttpResponse.json({ - * data: { viewer, siteConfig } + * data: { viewerSession, siteConfig } * }) * }, * requestOptions @@ -2762,28 +2960,6 @@ export const mockUserProfileQuery = (resolver: GraphQLResponseResolver { - * const { id } = variables; - * return HttpResponse.json({ - * data: { viewerSession, node } - * }) - * }, - * requestOptions - * ) - */ -export const mockSessionDetailQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => - graphql.query( - 'SessionDetail', - resolver, - options - ) - /** * @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions)) * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options)) @@ -2857,7 +3033,7 @@ export const mockAppSessionsListQuery = (resolver: GraphQLResponseResolver { * return HttpResponse.json({ - * data: { viewerSession, siteConfig } + * data: { viewer, siteConfig } * }) * }, * requestOptions @@ -3131,3 +3307,25 @@ export const mockAllowCrossSigningResetMutation = (resolver: GraphQLResponseReso resolver, options ) + +/** + * @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions)) + * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options)) + * @see https://mswjs.io/docs/basics/response-resolver + * @example + * mockSessionDetailQuery( + * ({ query, variables }) => { + * const { id } = variables; + * return HttpResponse.json({ + * data: { viewerSession, node } + * }) + * }, + * requestOptions + * ) + */ +export const mockSessionDetailQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) => + graphql.query( + 'SessionDetail', + resolver, + options + ) diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 2c412c68c..d36635c93 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as ResetCrossSigningImport } from './routes/reset-cross-signing' import { Route as AccountImport } from './routes/_account' import { Route as ResetCrossSigningIndexImport } from './routes/reset-cross-signing.index' import { Route as AccountIndexImport } from './routes/_account.index' +import { Route as SessionsIdImport } from './routes/sessions.$id' import { Route as ResetCrossSigningSuccessImport } from './routes/reset-cross-signing.success' import { Route as ResetCrossSigningCancelledImport } from './routes/reset-cross-signing.cancelled' import { Route as DevicesSplatImport } from './routes/devices.$' @@ -27,7 +28,6 @@ import { Route as AccountSessionsIndexImport } from './routes/_account.sessions. import { Route as EmailsIdVerifyImport } from './routes/emails.$id.verify' import { Route as EmailsIdInUseImport } from './routes/emails.$id.in-use' import { Route as AccountSessionsBrowsersImport } from './routes/_account.sessions.browsers' -import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id' // Create Virtual Routes @@ -62,6 +62,12 @@ const AccountIndexRoute = AccountIndexImport.update({ import('./routes/_account.index.lazy').then((d) => d.Route), ) +const SessionsIdRoute = SessionsIdImport.update({ + id: '/sessions/$id', + path: '/sessions/$id', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/sessions.$id.lazy').then((d) => d.Route)) + const ResetCrossSigningSuccessRoute = ResetCrossSigningSuccessImport.update({ id: '/success', path: '/success', @@ -142,14 +148,6 @@ const AccountSessionsBrowsersRoute = AccountSessionsBrowsersImport.update({ import('./routes/_account.sessions.browsers.lazy').then((d) => d.Route), ) -const AccountSessionsIdRoute = AccountSessionsIdImport.update({ - id: '/sessions/$id', - path: '/sessions/$id', - getParentRoute: () => AccountRoute, -} as any).lazy(() => - import('./routes/_account.sessions.$id.lazy').then((d) => d.Route), -) - // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -196,6 +194,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResetCrossSigningSuccessImport parentRoute: typeof ResetCrossSigningImport } + '/sessions/$id': { + id: '/sessions/$id' + path: '/sessions/$id' + fullPath: '/sessions/$id' + preLoaderRoute: typeof SessionsIdImport + parentRoute: typeof rootRoute + } '/_account/': { id: '/_account/' path: '/' @@ -210,13 +215,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResetCrossSigningIndexImport parentRoute: typeof ResetCrossSigningImport } - '/_account/sessions/$id': { - id: '/_account/sessions/$id' - path: '/sessions/$id' - fullPath: '/sessions/$id' - preLoaderRoute: typeof AccountSessionsIdImport - parentRoute: typeof AccountImport - } '/_account/sessions/browsers': { id: '/_account/sessions/browsers' path: '/sessions/browsers' @@ -273,14 +271,12 @@ declare module '@tanstack/react-router' { interface AccountRouteChildren { AccountIndexRoute: typeof AccountIndexRoute - AccountSessionsIdRoute: typeof AccountSessionsIdRoute AccountSessionsBrowsersRoute: typeof AccountSessionsBrowsersRoute AccountSessionsIndexRoute: typeof AccountSessionsIndexRoute } const AccountRouteChildren: AccountRouteChildren = { AccountIndexRoute: AccountIndexRoute, - AccountSessionsIdRoute: AccountSessionsIdRoute, AccountSessionsBrowsersRoute: AccountSessionsBrowsersRoute, AccountSessionsIndexRoute: AccountSessionsIndexRoute, } @@ -310,9 +306,9 @@ export interface FileRoutesByFullPath { '/devices/$': typeof DevicesSplatRoute '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute + '/sessions/$id': typeof SessionsIdRoute '/': typeof AccountIndexRoute '/reset-cross-signing/': typeof ResetCrossSigningIndexRoute - '/sessions/$id': typeof AccountSessionsIdRoute '/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/in-use': typeof EmailsIdInUseRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -327,9 +323,9 @@ export interface FileRoutesByTo { '/devices/$': typeof DevicesSplatRoute '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute + '/sessions/$id': typeof SessionsIdRoute '/': typeof AccountIndexRoute '/reset-cross-signing': typeof ResetCrossSigningIndexRoute - '/sessions/$id': typeof AccountSessionsIdRoute '/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/in-use': typeof EmailsIdInUseRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -347,9 +343,9 @@ export interface FileRoutesById { '/devices/$': typeof DevicesSplatRoute '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute + '/sessions/$id': typeof SessionsIdRoute '/_account/': typeof AccountIndexRoute '/reset-cross-signing/': typeof ResetCrossSigningIndexRoute - '/_account/sessions/$id': typeof AccountSessionsIdRoute '/_account/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/in-use': typeof EmailsIdInUseRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -368,9 +364,9 @@ export interface FileRouteTypes { | '/devices/$' | '/reset-cross-signing/cancelled' | '/reset-cross-signing/success' + | '/sessions/$id' | '/' | '/reset-cross-signing/' - | '/sessions/$id' | '/sessions/browsers' | '/emails/$id/in-use' | '/emails/$id/verify' @@ -384,9 +380,9 @@ export interface FileRouteTypes { | '/devices/$' | '/reset-cross-signing/cancelled' | '/reset-cross-signing/success' + | '/sessions/$id' | '/' | '/reset-cross-signing' - | '/sessions/$id' | '/sessions/browsers' | '/emails/$id/in-use' | '/emails/$id/verify' @@ -402,9 +398,9 @@ export interface FileRouteTypes { | '/devices/$' | '/reset-cross-signing/cancelled' | '/reset-cross-signing/success' + | '/sessions/$id' | '/_account/' | '/reset-cross-signing/' - | '/_account/sessions/$id' | '/_account/sessions/browsers' | '/emails/$id/in-use' | '/emails/$id/verify' @@ -420,6 +416,7 @@ export interface RootRouteChildren { ResetCrossSigningRoute: typeof ResetCrossSigningRouteWithChildren ClientsIdRoute: typeof ClientsIdRoute DevicesSplatRoute: typeof DevicesSplatRoute + SessionsIdRoute: typeof SessionsIdRoute EmailsIdInUseRoute: typeof EmailsIdInUseRoute EmailsIdVerifyRoute: typeof EmailsIdVerifyRoute PasswordChangeSuccessLazyRoute: typeof PasswordChangeSuccessLazyRoute @@ -432,6 +429,7 @@ const rootRouteChildren: RootRouteChildren = { ResetCrossSigningRoute: ResetCrossSigningRouteWithChildren, ClientsIdRoute: ClientsIdRoute, DevicesSplatRoute: DevicesSplatRoute, + SessionsIdRoute: SessionsIdRoute, EmailsIdInUseRoute: EmailsIdInUseRoute, EmailsIdVerifyRoute: EmailsIdVerifyRoute, PasswordChangeSuccessLazyRoute: PasswordChangeSuccessLazyRoute, @@ -453,6 +451,7 @@ export const routeTree = rootRoute "/reset-cross-signing", "/clients/$id", "/devices/$", + "/sessions/$id", "/emails/$id/in-use", "/emails/$id/verify", "/password/change/success", @@ -464,7 +463,6 @@ export const routeTree = rootRoute "filePath": "_account.tsx", "children": [ "/_account/", - "/_account/sessions/$id", "/_account/sessions/browsers", "/_account/sessions/" ] @@ -491,6 +489,9 @@ export const routeTree = rootRoute "filePath": "reset-cross-signing.success.tsx", "parent": "/reset-cross-signing" }, + "/sessions/$id": { + "filePath": "sessions.$id.tsx" + }, "/_account/": { "filePath": "_account.index.tsx", "parent": "/_account" @@ -499,10 +500,6 @@ export const routeTree = rootRoute "filePath": "reset-cross-signing.index.tsx", "parent": "/reset-cross-signing" }, - "/_account/sessions/$id": { - "filePath": "_account.sessions.$id.tsx", - "parent": "/_account" - }, "/_account/sessions/browsers": { "filePath": "_account.sessions.browsers.tsx", "parent": "/_account" diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx index f831f19d3..46e52fbf9 100644 --- a/frontend/src/routes/_account.index.lazy.tsx +++ b/frontend/src/routes/_account.index.lazy.tsx @@ -10,28 +10,63 @@ import { notFound, useNavigate, } from "@tanstack/react-router"; -import { Separator, Text } from "@vector-im/compound-web"; +import IconSignOut from "@vector-im/compound-design-tokens/assets/web/icons/sign-out"; +import { Button, Separator, Text } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; - import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview"; import { ButtonLink } from "../components/ButtonLink"; import * as Collapsible from "../components/Collapsible"; +import * as Dialog from "../components/Dialog"; +import LoadingSpinner from "../components/LoadingSpinner"; +import { useEndBrowserSession } from "../components/Session/EndBrowserSessionButton"; import AddEmailForm from "../components/UserProfile/AddEmailForm"; import UserEmailList from "../components/UserProfile/UserEmailList"; - import { query } from "./_account.index"; export const Route = createLazyFileRoute("/_account/")({ component: Index, }); +const SignOutButton: React.FC<{ id: string }> = ({ id }) => { + const { t } = useTranslation(); + const mutation = useEndBrowserSession(id, true); + + return ( + + {t("frontend.account.sign_out.button")} + + } + > + {t("frontend.account.sign_out.dialog")} + + + + + + + + ); +}; + function Index(): React.ReactElement { const navigate = useNavigate(); const { t } = useTranslation(); const { - data: { viewer, siteConfig }, + data: { viewerSession, siteConfig }, } = useSuspenseQuery(query); - if (viewer?.__typename !== "User") throw notFound(); + if (viewerSession?.__typename !== "BrowserSession") throw notFound(); // When adding an email, we want to go to the email verification form const onAdd = async (id: string): Promise => { @@ -39,45 +74,52 @@ function Index(): React.ReactElement { }; return ( -
    - {/* Only display this section if the user can add email addresses to their + <> +
    + {/* Only display this section if the user can add email addresses to their account *or* if they have any existing email addresses */} - {(siteConfig.emailChangeAllowed || viewer.emails.totalCount > 0) && ( - <> - - - - {siteConfig.emailChangeAllowed && } - - - - - )} - - {siteConfig.passwordLoginEnabled && ( - <> - - - - - - - )} - - - - {t("frontend.reset_cross_signing.description")} - - - {t("frontend.reset_cross_signing.start_reset")} - - -
    + {(siteConfig.emailChangeAllowed || + viewerSession.user.emails.totalCount > 0) && ( + <> + + + + {siteConfig.emailChangeAllowed && } + + + + + )} + + {siteConfig.passwordLoginEnabled && ( + <> + + + + + + + )} + + + + {t("frontend.reset_cross_signing.description")} + + + {t("frontend.reset_cross_signing.start_reset")} + + + + +
    + + + ); } diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx index bbd0a41d5..cd9a12271 100644 --- a/frontend/src/routes/_account.index.tsx +++ b/frontend/src/routes/_account.index.tsx @@ -13,11 +13,14 @@ import { graphqlRequest } from "../graphql"; const QUERY = graphql(/* GraphQL */ ` query UserProfile { - viewer { + viewerSession { __typename - ... on User { - emails(first: 0) { - totalCount + ... on BrowserSession { + id + user { + emails(first: 0) { + totalCount + } } } } diff --git a/frontend/src/routes/_account.lazy.tsx b/frontend/src/routes/_account.lazy.tsx index 3acf066fd..817a6fc91 100644 --- a/frontend/src/routes/_account.lazy.tsx +++ b/frontend/src/routes/_account.lazy.tsx @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // // SPDX-License-Identifier: AGPL-3.0-only @@ -7,12 +7,9 @@ import { Outlet, createLazyFileRoute, notFound } from "@tanstack/react-router"; import { Heading } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; - -import { useEndBrowserSession } from "../components/BrowserSession"; import Layout from "../components/Layout"; import NavBar from "../components/NavBar"; import NavItem from "../components/NavItem"; -import EndSessionButton from "../components/Session/EndSessionButton"; import UserGreeting from "../components/UserGreeting"; import { useSuspenseQuery } from "@tanstack/react-query"; @@ -25,24 +22,19 @@ export const Route = createLazyFileRoute("/_account")({ function Account(): React.ReactElement { const { t } = useTranslation(); const result = useSuspenseQuery(query); - const session = result.data.viewerSession; - if (session?.__typename !== "BrowserSession") throw notFound(); + const viewer = result.data.viewer; + if (viewer?.__typename !== "User") throw notFound(); const siteConfig = result.data.siteConfig; - const onSessionEnd = useEndBrowserSession(session.id, true); return (
    -
    - - {t("frontend.account.title")} - - - -
    + + {t("frontend.account.title")} +
    - + {t("frontend.nav.settings")} diff --git a/frontend/src/routes/_account.sessions.browsers.lazy.tsx b/frontend/src/routes/_account.sessions.browsers.lazy.tsx index eefcf9dbc..ee299aaa1 100644 --- a/frontend/src/routes/_account.sessions.browsers.lazy.tsx +++ b/frontend/src/routes/_account.sessions.browsers.lazy.tsx @@ -8,7 +8,6 @@ import { createLazyFileRoute, notFound } from "@tanstack/react-router"; import { H5 } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; -import BlockList from "../components/BlockList"; import BrowserSession from "../components/BrowserSession"; import { ButtonLink } from "../components/ButtonLink"; import EmptyState from "../components/EmptyState"; @@ -42,7 +41,7 @@ function BrowserSessions(): React.ReactElement { // We reverse the list as we are paginating backwards const edges = [...viewerSession.user.browserSessions.edges].reverse(); return ( - +
    {t("frontend.browser_sessions_overview.heading")}
    @@ -104,6 +103,6 @@ function BrowserSessions(): React.ReactElement {
    )} - +
    ); } diff --git a/frontend/src/routes/_account.sessions.index.lazy.tsx b/frontend/src/routes/_account.sessions.index.lazy.tsx index 4f1b8fa5a..6c328dd79 100644 --- a/frontend/src/routes/_account.sessions.index.lazy.tsx +++ b/frontend/src/routes/_account.sessions.index.lazy.tsx @@ -8,7 +8,6 @@ import { createLazyFileRoute, notFound } from "@tanstack/react-router"; import { H3, Separator } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; -import BlockList from "../components/BlockList"; import { ButtonLink } from "../components/ButtonLink"; import CompatSession from "../components/CompatSession"; import EmptyState from "../components/EmptyState"; @@ -53,7 +52,7 @@ function Sessions(): React.ReactElement { const edges = [...appSessions.edges].reverse(); return ( - +

    {t("frontend.user_sessions_overview.heading")}

    @@ -121,6 +120,6 @@ function Sessions(): React.ReactElement {
    )} -
    +
    ); } diff --git a/frontend/src/routes/_account.tsx b/frontend/src/routes/_account.tsx index e61b98aaa..9c7bdc714 100644 --- a/frontend/src/routes/_account.tsx +++ b/frontend/src/routes/_account.tsx @@ -11,15 +11,10 @@ import { graphqlRequest } from "../graphql"; const QUERY = graphql(/* GraphQL */ ` query CurrentUserGreeting { - viewerSession { + viewer { __typename - - ... on BrowserSession { - id - - user { - ...UserGreeting_user - } + ... on User { + ...UserGreeting_user } } diff --git a/frontend/src/routes/password.change.index.lazy.tsx b/frontend/src/routes/password.change.index.lazy.tsx index a66e6c65e..b27f94c21 100644 --- a/frontend/src/routes/password.change.index.lazy.tsx +++ b/frontend/src/routes/password.change.index.lazy.tsx @@ -15,7 +15,6 @@ import { Alert, Form, Separator } from "@vector-im/compound-web"; import { type FormEvent, useRef } from "react"; import { useTranslation } from "react-i18next"; -import BlockList from "../components/BlockList"; import { ButtonLink } from "../components/ButtonLink"; import Layout from "../components/Layout"; import LoadingSpinner from "../components/LoadingSpinner"; @@ -103,7 +102,7 @@ function ChangePassword(): React.ReactNode { return ( - +
    - +
    ); } diff --git a/frontend/src/routes/password.change.success.lazy.tsx b/frontend/src/routes/password.change.success.lazy.tsx index 1638189e6..15357e0e0 100644 --- a/frontend/src/routes/password.change.success.lazy.tsx +++ b/frontend/src/routes/password.change.success.lazy.tsx @@ -7,8 +7,6 @@ import { createLazyFileRoute } from "@tanstack/react-router"; import IconCheckCircle from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; import { useTranslation } from "react-i18next"; - -import BlockList from "../components/BlockList"; import { ButtonLink } from "../components/ButtonLink"; import Layout from "../components/Layout"; import PageHeading from "../components/PageHeading"; @@ -22,7 +20,7 @@ function ChangePasswordSuccess(): React.ReactNode { return ( - +
    {t("action.back")} - +
    ); } diff --git a/frontend/src/routes/password.recovery.index.lazy.tsx b/frontend/src/routes/password.recovery.index.lazy.tsx index 3b16d6d1a..c97d7ab53 100644 --- a/frontend/src/routes/password.recovery.index.lazy.tsx +++ b/frontend/src/routes/password.recovery.index.lazy.tsx @@ -16,8 +16,6 @@ import IconLockSolid from "@vector-im/compound-design-tokens/assets/web/icons/lo import { Alert, Button, Form } from "@vector-im/compound-web"; import type { FormEvent } from "react"; import { useTranslation } from "react-i18next"; - -import BlockList from "../components/BlockList"; import { ButtonLink } from "../components/ButtonLink"; import Layout from "../components/Layout"; import LoadingSpinner from "../components/LoadingSpinner"; @@ -206,7 +204,7 @@ const EmailRecovery: React.FC<{ return ( - +
    - +
    ); }; diff --git a/frontend/src/routes/reset-cross-signing.index.tsx b/frontend/src/routes/reset-cross-signing.index.tsx index db7150faf..ce83780cf 100644 --- a/frontend/src/routes/reset-cross-signing.index.tsx +++ b/frontend/src/routes/reset-cross-signing.index.tsx @@ -13,15 +13,16 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import IconCheck from "@vector-im/compound-design-tokens/assets/web/icons/check"; import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info"; -import { Button, Text } from "@vector-im/compound-web"; +import { + Button, + Text, + VisualList, + VisualListItem, +} from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; import { ButtonLink } from "../components/ButtonLink"; import LoadingSpinner from "../components/LoadingSpinner"; import PageHeading from "../components/PageHeading"; -import { - VisualList, - VisualListItem, -} from "../components/VisualList/VisualList"; import { graphql } from "../gql"; import { graphqlRequest } from "../graphql"; @@ -123,19 +124,15 @@ function ResetCrossSigning(): React.ReactNode { - - - + + {t("frontend.reset_cross_signing.effect_list.positive_1")} + + + {t("frontend.reset_cross_signing.effect_list.neutral_1")} + + + {t("frontend.reset_cross_signing.effect_list.neutral_2")} + diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx index e657d34ed..8a5a1c800 100644 --- a/frontend/src/routes/reset-cross-signing.tsx +++ b/frontend/src/routes/reset-cross-signing.tsx @@ -13,7 +13,6 @@ import { Button, Text } from "@vector-im/compound-web"; import * as v from "valibot"; import { useTranslation } from "react-i18next"; -import BlockList from "../components/BlockList"; import Layout from "../components/Layout"; import PageHeading from "../components/PageHeading"; @@ -25,9 +24,7 @@ export const Route = createFileRoute("/reset-cross-signing")({ validateSearch: searchSchema, component: () => ( - - - + ), errorComponent: ResetCrossSigningError, diff --git a/frontend/src/routes/_account.sessions.$id.lazy.tsx b/frontend/src/routes/sessions.$id.lazy.tsx similarity index 61% rename from frontend/src/routes/_account.sessions.$id.lazy.tsx rename to frontend/src/routes/sessions.$id.lazy.tsx index 35e3201a9..bc9524bf2 100644 --- a/frontend/src/routes/_account.sessions.$id.lazy.tsx +++ b/frontend/src/routes/sessions.$id.lazy.tsx @@ -4,19 +4,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useSuspenseQuery } from "@tanstack/react-query"; import { createLazyFileRoute, notFound } from "@tanstack/react-router"; import { Alert } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; - +import Layout from "../components/Layout"; import { Link } from "../components/Link"; import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail"; import CompatSessionDetail from "../components/SessionDetail/CompatSessionDetail"; import OAuth2SessionDetail from "../components/SessionDetail/OAuth2SessionDetail"; +import { query } from "./sessions.$id"; -import { useSuspenseQuery } from "@tanstack/react-query"; -import { query } from "./_account.sessions.$id"; - -export const Route = createLazyFileRoute("/_account/sessions/$id")({ +export const Route = createLazyFileRoute("/sessions/$id")({ notFoundComponent: NotFound, component: SessionDetail, }); @@ -26,13 +25,15 @@ function NotFound(): React.ReactElement { const { t } = useTranslation(); return ( - - {t("frontend.session_detail.alert.text")} - {t("frontend.session_detail.alert.button")} - + + + {t("frontend.session_detail.alert.text")} + {t("frontend.session_detail.alert.button")} + + ); } @@ -45,15 +46,25 @@ function SessionDetail(): React.ReactElement { switch (node.__typename) { case "CompatSession": - return ; + return ( + + + + ); case "Oauth2Session": - return ; + return ( + + + + ); case "BrowserSession": return ( - + + + ); default: throw new Error("Unknown session type"); diff --git a/frontend/src/routes/_account.sessions.$id.tsx b/frontend/src/routes/sessions.$id.tsx similarity index 93% rename from frontend/src/routes/_account.sessions.$id.tsx rename to frontend/src/routes/sessions.$id.tsx index b1d4668d3..9139aed31 100644 --- a/frontend/src/routes/_account.sessions.$id.tsx +++ b/frontend/src/routes/sessions.$id.tsx @@ -34,7 +34,7 @@ export const query = (id: string) => graphqlRequest({ query: QUERY, signal, variables: { id } }), }); -export const Route = createFileRoute("/_account/sessions/$id")({ +export const Route = createFileRoute("/sessions/$id")({ loader: ({ context, params }) => context.queryClient.ensureQueryData(query(params.id)), }); diff --git a/frontend/src/utils/simplifyUrl.ts b/frontend/src/utils/simplifyUrl.ts new file mode 100644 index 000000000..2319ebfb0 --- /dev/null +++ b/frontend/src/utils/simplifyUrl.ts @@ -0,0 +1,33 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +/** + * Simplify a URL by removing the protocol, search params and hash. + * + * @param url The URL to simplify + * @returns The simplified URL + */ +const simplifyUrl = (url: string): string => { + let parsed: URL; + try { + parsed = new URL(url); + } catch (_e) { + // Not a valid URL, return the original + return url; + } + + // Clear out the search params and hash + parsed.search = ""; + parsed.hash = ""; + + if (parsed.protocol === "https:") { + return parsed.hostname; + } + + // Return the simplified URL + return parsed.toString(); +}; + +export default simplifyUrl; diff --git a/frontend/stories/routes/index.stories.tsx b/frontend/stories/routes/index.stories.tsx index 9156ff0fa..15fb4dedd 100644 --- a/frontend/stories/routes/index.stories.tsx +++ b/frontend/stories/routes/index.stories.tsx @@ -43,10 +43,13 @@ const userProfileHandler = ({ mockUserProfileQuery(() => HttpResponse.json({ data: { - viewer: { - __typename: "User", - emails: { - totalCount: emailTotalCount, + viewerSession: { + __typename: "BrowserSession", + id: "session-id", + user: { + emails: { + totalCount: emailTotalCount, + }, }, }, diff --git a/frontend/tests/mocks/handlers.ts b/frontend/tests/mocks/handlers.ts index dcba3b5cc..b89347ccc 100644 --- a/frontend/tests/mocks/handlers.ts +++ b/frontend/tests/mocks/handlers.ts @@ -60,23 +60,19 @@ export const handlers = [ mockCurrentUserGreetingQuery(() => HttpResponse.json({ data: { - viewerSession: { - __typename: "BrowserSession", - - id: "session-id", - user: Object.assign( - makeFragmentData( - { - id: "user-id", - matrix: { - mxid: "@alice:example.com", - displayName: "Alice", - }, + viewer: Object.assign( + makeFragmentData( + { + __typename: "User", + id: "user-id", + matrix: { + mxid: "@alice:example.com", + displayName: "Alice", }, - USER_GREETING_FRAGMENT, - ), + }, + USER_GREETING_FRAGMENT, ), - }, + ), siteConfig: makeFragmentData( { @@ -91,10 +87,13 @@ export const handlers = [ mockUserProfileQuery(() => HttpResponse.json({ data: { - viewer: { - __typename: "User", - emails: { - totalCount: 1, + viewerSession: { + __typename: "BrowserSession", + id: "browser-session-id", + user: { + emails: { + totalCount: 1, + }, }, }, diff --git a/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap index 5964df356..f29732dfd 100644 --- a/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap +++ b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap @@ -5,48 +5,44 @@ exports[`Reset cross signing > renders the cancelled page 1`] = `
    -
    -
    -
    - - - -
    -
    -

    - Identity reset cancelled. -

    -
    -
    -

    - You can close this window and go back to the app to continue. -

    -

    + +

    +
    - If you're signed out everywhere and don't remember your recovery code, you'll still need to reset your identity. -

    -
    +

    + Identity reset cancelled. +

    +
    + +

    + You can close this window and go back to the app to continue. +

    +

    + If you're signed out everywhere and don't remember your recovery code, you'll still need to reset your identity. +