You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-23 11:02:35 +03:00
browser session detail page
This commit is contained in:
committed by
Quentin Gliech
parent
0d5a700182
commit
de04b1679c
@@ -15,6 +15,7 @@
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithMutation } from "jotai-urql";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms";
|
||||
import { FragmentType, graphql, useFragment } from "../gql";
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
import EndSessionButton from "./Session/EndSessionButton";
|
||||
import Session from "./Session/Session";
|
||||
|
||||
const FRAGMENT = graphql(/* GraphQL */ `
|
||||
export const BROWSER_SESSION_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment BrowserSession_session on BrowserSession {
|
||||
id
|
||||
createdAt
|
||||
@@ -52,7 +53,7 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const endSessionFamily = atomFamily((id: string) => {
|
||||
export const endBrowserSessionFamily = atomFamily((id: string) => {
|
||||
const endSession = atomWithMutation(END_SESSION_MUTATION);
|
||||
|
||||
// A proxy atom which pre-sets the id variable in the mutation
|
||||
@@ -64,22 +65,17 @@ const endSessionFamily = atomFamily((id: string) => {
|
||||
return endSessionAtom;
|
||||
});
|
||||
|
||||
type Props = {
|
||||
session: FragmentType<typeof FRAGMENT>;
|
||||
isCurrent: boolean;
|
||||
};
|
||||
|
||||
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
const data = useFragment(FRAGMENT, session);
|
||||
const endSession = useSetAtom(endSessionFamily(data.id));
|
||||
export const useEndBrowserSession = (
|
||||
sessionId: string,
|
||||
isCurrent: boolean,
|
||||
): (() => Promise<void>) => {
|
||||
const endSession = useSetAtom(endBrowserSessionFamily(sessionId));
|
||||
|
||||
// Pull those atoms to reset them when the current session is ended
|
||||
const currentUserId = useSetAtom(currentUserIdAtom);
|
||||
const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom);
|
||||
|
||||
const createdAt = data.createdAt;
|
||||
|
||||
const onSessionEnd = async (): Promise<void> => {
|
||||
const onSessionEnd = useCallback(async (): Promise<void> => {
|
||||
await endSession();
|
||||
if (isCurrent) {
|
||||
currentBrowserSessionId({
|
||||
@@ -89,8 +85,22 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [isCurrent, endSession, currentBrowserSessionId, currentUserId]);
|
||||
|
||||
return onSessionEnd;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
session: FragmentType<typeof BROWSER_SESSION_FRAGMENT>;
|
||||
isCurrent: boolean;
|
||||
};
|
||||
|
||||
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
const data = useFragment(BROWSER_SESSION_FRAGMENT, session);
|
||||
|
||||
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
||||
|
||||
const createdAt = data.createdAt;
|
||||
const deviceInformation = parseUserAgent(data.userAgent || undefined);
|
||||
const sessionName =
|
||||
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { currentBrowserSessionIdAtom, mapQueryAtom } from "../atoms";
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import { graphql } from "../gql";
|
||||
import { BrowserSessionState, PageInfo } from "../gql/graphql";
|
||||
import {
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Pagination,
|
||||
} from "../pagination";
|
||||
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
||||
import { useCurrentBrowserSessionId } from "../utils/session/useCurrentBrowserSessionId";
|
||||
|
||||
import BlockList from "./BlockList";
|
||||
import BrowserSession from "./BrowserSession";
|
||||
@@ -112,20 +113,20 @@ const paginationFamily = atomFamily((userId: string) => {
|
||||
});
|
||||
|
||||
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const currentSessionIdResult = useAtomValue(currentBrowserSessionIdAtom);
|
||||
const { currentBrowserSessionId, currentBrowserSessionIdError } =
|
||||
useCurrentBrowserSessionId();
|
||||
const [pending, startTransition] = useTransition();
|
||||
const result = useAtomValue(browserSessionListFamily(userId));
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
const [filter, setFilter] = useAtom(filterAtom);
|
||||
|
||||
if (isErr(currentSessionIdResult))
|
||||
return <GraphQLError error={unwrapErr(currentSessionIdResult)} />;
|
||||
if (currentBrowserSessionIdError)
|
||||
return <GraphQLError error={currentBrowserSessionIdError} />;
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const browserSessions = unwrapOk(result);
|
||||
if (browserSessions === null) return <>Failed to load browser sessions</>;
|
||||
const currentSessionId = unwrapOk(currentSessionIdResult);
|
||||
|
||||
const paginate = (pagination: Pagination): void => {
|
||||
startTransition(() => {
|
||||
@@ -165,7 +166,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
<BrowserSession
|
||||
key={n.cursor}
|
||||
session={n.node}
|
||||
isCurrent={n.node.id === currentSessionId}
|
||||
isCurrent={n.node.id === currentBrowserSessionId}
|
||||
/>
|
||||
))}
|
||||
</BlockList>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--cpd-space-1x);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { H3, Badge } from "@vector-im/compound-web";
|
||||
|
||||
import { FragmentType, useFragment } from "../../gql";
|
||||
import { BROWSER_SESSION_DETAIL_FRAGMENT } from "../../pages/BrowserSession";
|
||||
import {
|
||||
parseUserAgent,
|
||||
sessionNameFromDeviceInformation,
|
||||
} from "../../utils/parseUserAgent";
|
||||
import { useCurrentBrowserSessionId } from "../../utils/session/useCurrentBrowserSessionId";
|
||||
import BlockList from "../BlockList/BlockList";
|
||||
import { useEndBrowserSession } from "../BrowserSession";
|
||||
import DateTime from "../DateTime";
|
||||
import GraphQLError from "../GraphQLError";
|
||||
import EndSessionButton from "../Session/EndSessionButton";
|
||||
|
||||
import styles from "./BrowserSessionDetail.module.css";
|
||||
import SessionDetails from "./SessionDetails";
|
||||
|
||||
type Props = {
|
||||
session: FragmentType<typeof BROWSER_SESSION_DETAIL_FRAGMENT>;
|
||||
};
|
||||
|
||||
const BrowserSessionDetail: React.FC<Props> = ({ session }) => {
|
||||
const data = useFragment(BROWSER_SESSION_DETAIL_FRAGMENT, session);
|
||||
const { currentBrowserSessionId, currentBrowserSessionIdError } =
|
||||
useCurrentBrowserSessionId();
|
||||
|
||||
const isCurrent = currentBrowserSessionId === data.id;
|
||||
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
||||
|
||||
if (currentBrowserSessionIdError)
|
||||
return <GraphQLError error={currentBrowserSessionIdError} />;
|
||||
|
||||
const deviceInformation = parseUserAgent(data.userAgent || undefined);
|
||||
const sessionName =
|
||||
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
|
||||
|
||||
const finishedAt = data.finishedAt
|
||||
? [{ label: "Finished", value: <DateTime datetime={data.finishedAt} /> }]
|
||||
: [];
|
||||
|
||||
const latestAuthentication = data.lastAuthentication
|
||||
? [
|
||||
{
|
||||
label: "Last Authentication",
|
||||
value: <DateTime datetime={data.lastAuthentication.createdAt} />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sessionDetails = [
|
||||
{ label: "ID", value: <code>{data.id}</code> },
|
||||
{ label: "User ID", value: <code>{data.user.id}</code> },
|
||||
{ label: "User Name", value: <code>{data.user.username}</code> },
|
||||
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
|
||||
...finishedAt,
|
||||
...latestAuthentication,
|
||||
];
|
||||
|
||||
return (
|
||||
<BlockList>
|
||||
<header className={styles.header}>
|
||||
{isCurrent && <Badge kind="success">Current</Badge>}
|
||||
<H3>{sessionName}</H3>
|
||||
</header>
|
||||
<SessionDetails title="Session" details={sessionDetails} />
|
||||
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
|
||||
</BlockList>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowserSessionDetail;
|
||||
@@ -65,7 +65,9 @@ const documents = {
|
||||
types.VerifyEmailDocument,
|
||||
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
|
||||
types.ResendVerificationEmailDocument,
|
||||
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
|
||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n":
|
||||
types.BrowserSession_DetailFragmentDoc,
|
||||
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n ...BrowserSession_detail\n }\n }\n":
|
||||
types.BrowserSessionQueryDocument,
|
||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
|
||||
types.OAuth2ClientQueryDocument,
|
||||
@@ -249,8 +251,14 @@ export function graphql(
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"];
|
||||
source: "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n",
|
||||
): (typeof documents)["\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n ...BrowserSession_detail\n }\n }\n",
|
||||
): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n ...BrowserSession_detail\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1516,23 +1516,33 @@ export type ResendVerificationEmailMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type BrowserSession_DetailFragment = {
|
||||
__typename?: "BrowserSession";
|
||||
id: string;
|
||||
createdAt: any;
|
||||
finishedAt?: any | null;
|
||||
userAgent?: string | null;
|
||||
lastAuthentication?: {
|
||||
__typename?: "Authentication";
|
||||
id: string;
|
||||
createdAt: any;
|
||||
} | null;
|
||||
user: { __typename?: "User"; id: string; username: string };
|
||||
} & { " $fragmentName"?: "BrowserSession_DetailFragment" };
|
||||
|
||||
export type BrowserSessionQueryQueryVariables = Exact<{
|
||||
id: Scalars["ID"]["input"];
|
||||
}>;
|
||||
|
||||
export type BrowserSessionQueryQuery = {
|
||||
__typename?: "Query";
|
||||
browserSession?: {
|
||||
__typename?: "BrowserSession";
|
||||
id: string;
|
||||
createdAt: any;
|
||||
lastAuthentication?: {
|
||||
__typename?: "Authentication";
|
||||
id: string;
|
||||
createdAt: any;
|
||||
} | null;
|
||||
user: { __typename?: "User"; id: string; username: string };
|
||||
} | null;
|
||||
browserSession?:
|
||||
| ({ __typename?: "BrowserSession"; id: string } & {
|
||||
" $fragmentRefs"?: {
|
||||
BrowserSession_DetailFragment: BrowserSession_DetailFragment;
|
||||
};
|
||||
})
|
||||
| null;
|
||||
};
|
||||
|
||||
export type OAuth2ClientQueryQueryVariables = Exact<{
|
||||
@@ -1929,6 +1939,50 @@ export const UserEmail_VerifyEmailFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
|
||||
export const BrowserSession_DetailFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "BrowserSession_detail" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "BrowserSession" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "lastAuthentication" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "user" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "username" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<BrowserSession_DetailFragment, unknown>;
|
||||
export const CurrentViewerQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -4152,39 +4206,53 @@ export const BrowserSessionQueryDocument = {
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "BrowserSession_detail" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "BrowserSession_detail" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "BrowserSession" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "lastAuthentication" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "lastAuthentication" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "createdAt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "user" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "username" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "user" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "username" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,22 +19,32 @@ import { atomWithQuery } from "jotai-urql";
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
export const BROWSER_SESSION_DETAIL_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment BrowserSession_detail on BrowserSession {
|
||||
id
|
||||
createdAt
|
||||
finishedAt
|
||||
userAgent
|
||||
lastAuthentication {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query BrowserSessionQuery($id: ID!) {
|
||||
browserSession(id: $id) {
|
||||
id
|
||||
createdAt
|
||||
lastAuthentication {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
...BrowserSession_detail
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -58,13 +68,9 @@ const BrowserSession: React.FC<{ id: string }> = ({ id }) => {
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const browserSession = unwrapOk(result);
|
||||
if (browserSession === null) return <NotFound />;
|
||||
if (!browserSession) return <NotFound />;
|
||||
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(browserSession, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
return <BrowserSessionDetail session={browserSession} />;
|
||||
};
|
||||
|
||||
export default BrowserSession;
|
||||
|
||||
Reference in New Issue
Block a user