1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-23 11:02:35 +03:00

Make sure we load current session data in route loaders

This means we should almost never see a loading spinner when navigating
This commit is contained in:
Quentin Gliech
2024-02-19 17:18:52 +01:00
parent 800c594276
commit e76945372a
7 changed files with 230 additions and 155 deletions

View File

@@ -21,7 +21,6 @@ import {
parseUserAgent, parseUserAgent,
sessionNameFromDeviceInformation, sessionNameFromDeviceInformation,
} from "../utils/parseUserAgent"; } from "../utils/parseUserAgent";
import { useCurrentBrowserSessionId } from "../utils/session/useCurrentBrowserSessionId";
import EndSessionButton from "./Session/EndSessionButton"; import EndSessionButton from "./Session/EndSessionButton";
import Session from "./Session/Session"; import Session from "./Session/Session";
@@ -71,12 +70,11 @@ export const useEndBrowserSession = (
type Props = { type Props = {
session: FragmentType<typeof FRAGMENT>; session: FragmentType<typeof FRAGMENT>;
isCurrent: boolean;
}; };
const BrowserSession: React.FC<Props> = ({ session }) => { const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
const currentBrowserSessionId = useCurrentBrowserSessionId();
const data = useFragment(FRAGMENT, session); const data = useFragment(FRAGMENT, session);
const isCurrent = data.id === currentBrowserSessionId;
const onSessionEnd = useEndBrowserSession(data.id, isCurrent); const onSessionEnd = useEndBrowserSession(data.id, isCurrent);

View File

@@ -21,7 +21,6 @@ import {
parseUserAgent, parseUserAgent,
sessionNameFromDeviceInformation, sessionNameFromDeviceInformation,
} from "../../utils/parseUserAgent"; } from "../../utils/parseUserAgent";
import { useCurrentBrowserSessionId } from "../../utils/session/useCurrentBrowserSessionId";
import BlockList from "../BlockList/BlockList"; import BlockList from "../BlockList/BlockList";
import { useEndBrowserSession } from "../BrowserSession"; import { useEndBrowserSession } from "../BrowserSession";
import DateTime from "../DateTime"; import DateTime from "../DateTime";
@@ -53,14 +52,13 @@ const FRAGMENT = graphql(/* GraphQL */ `
type Props = { type Props = {
session: FragmentType<typeof FRAGMENT>; session: FragmentType<typeof FRAGMENT>;
isCurrent: boolean;
}; };
const BrowserSessionDetail: React.FC<Props> = ({ session }) => { const BrowserSessionDetail: React.FC<Props> = ({ session, isCurrent }) => {
const data = useFragment(FRAGMENT, session); const data = useFragment(FRAGMENT, session);
const currentBrowserSessionId = useCurrentBrowserSessionId();
const { t } = useTranslation(); const { t } = useTranslation();
const isCurrent = currentBrowserSessionId === data.id;
const onSessionEnd = useEndBrowserSession(data.id, isCurrent); const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
const deviceInformation = parseUserAgent(data.userAgent || undefined); const deviceInformation = parseUserAgent(data.userAgent || undefined);

View File

@@ -63,9 +63,9 @@ const documents = {
types.ResendVerificationEmailDocument, types.ResendVerificationEmailDocument,
"\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserName_user\n ...UserEmailList_user\n }\n }\n }\n": "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserName_user\n ...UserEmailList_user\n }\n }\n }\n":
types.UserProfileQueryDocument, types.UserProfileQueryDocument,
"\n query SessionDetailQuery($id: ID!) {\n node(id: $id) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": "\n query SessionDetailQuery($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.SessionDetailQueryDocument, types.SessionDetailQueryDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\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 query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\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 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, types.BrowserSessionListDocument,
"\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": "\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n":
types.SessionsOverviewQueryDocument, types.SessionsOverviewQueryDocument,
@@ -255,14 +255,14 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query SessionDetailQuery($id: ID!) {\n node(id: $id) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n", source: "\n query SessionDetailQuery($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 documents)["\n query SessionDetailQuery($id: ID!) {\n node(id: $id) {\n __typename\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n"]; ): (typeof documents)["\n query SessionDetailQuery($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"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\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", source: "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\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 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 documents)["\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\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"]; ): (typeof documents)["\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\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 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"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@@ -1510,30 +1510,34 @@ export type SessionDetailQueryQueryVariables = Exact<{
export type SessionDetailQueryQuery = { export type SessionDetailQueryQuery = {
__typename?: "Query"; __typename?: "Query";
viewerSession:
| { __typename?: "Anonymous"; id: string }
| { __typename?: "BrowserSession"; id: string }
| { __typename?: "Oauth2Session"; id: string };
node?: node?:
| { __typename: "Anonymous" } | { __typename: "Anonymous"; id: string }
| { __typename: "Authentication" } | { __typename: "Authentication"; id: string }
| ({ __typename: "BrowserSession" } & { | ({ __typename: "BrowserSession"; id: string } & {
" $fragmentRefs"?: { " $fragmentRefs"?: {
BrowserSession_DetailFragment: BrowserSession_DetailFragment; BrowserSession_DetailFragment: BrowserSession_DetailFragment;
}; };
}) })
| ({ __typename: "CompatSession" } & { | ({ __typename: "CompatSession"; id: string } & {
" $fragmentRefs"?: { " $fragmentRefs"?: {
CompatSession_DetailFragment: CompatSession_DetailFragment; CompatSession_DetailFragment: CompatSession_DetailFragment;
}; };
}) })
| { __typename: "CompatSsoLogin" } | { __typename: "CompatSsoLogin"; id: string }
| { __typename: "Oauth2Client" } | { __typename: "Oauth2Client"; id: string }
| ({ __typename: "Oauth2Session" } & { | ({ __typename: "Oauth2Session"; id: string } & {
" $fragmentRefs"?: { " $fragmentRefs"?: {
OAuth2Session_DetailFragment: OAuth2Session_DetailFragment; OAuth2Session_DetailFragment: OAuth2Session_DetailFragment;
}; };
}) })
| { __typename: "UpstreamOAuth2Link" } | { __typename: "UpstreamOAuth2Link"; id: string }
| { __typename: "UpstreamOAuth2Provider" } | { __typename: "UpstreamOAuth2Provider"; id: string }
| { __typename: "User" } | { __typename: "User"; id: string }
| { __typename: "UserEmail" } | { __typename: "UserEmail"; id: string }
| null; | null;
}; };
@@ -1546,32 +1550,37 @@ export type BrowserSessionListQueryVariables = Exact<{
export type BrowserSessionListQuery = { export type BrowserSessionListQuery = {
__typename?: "Query"; __typename?: "Query";
viewer: viewerSession:
| { __typename: "Anonymous" } | { __typename: "Anonymous" }
| { | {
__typename: "User"; __typename: "BrowserSession";
id: string; id: string;
browserSessions: { user: {
__typename?: "BrowserSessionConnection"; __typename?: "User";
totalCount: number; id: string;
edges: Array<{ browserSessions: {
__typename?: "BrowserSessionEdge"; __typename?: "BrowserSessionConnection";
cursor: string; totalCount: number;
node: { __typename?: "BrowserSession"; id: string } & { edges: Array<{
" $fragmentRefs"?: { __typename?: "BrowserSessionEdge";
BrowserSession_SessionFragment: BrowserSession_SessionFragment; cursor: string;
node: { __typename?: "BrowserSession"; id: string } & {
" $fragmentRefs"?: {
BrowserSession_SessionFragment: BrowserSession_SessionFragment;
};
}; };
}>;
pageInfo: {
__typename?: "PageInfo";
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string | null;
endCursor?: string | null;
}; };
}>;
pageInfo: {
__typename?: "PageInfo";
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string | null;
endCursor?: string | null;
}; };
}; };
}; }
| { __typename: "Oauth2Session" };
}; };
export type SessionsOverviewQueryQueryVariables = Exact<{ export type SessionsOverviewQueryQueryVariables = Exact<{
@@ -3417,6 +3426,28 @@ export const SessionDetailQueryDocument = {
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
{
kind: "Field",
name: { kind: "Name", value: "viewerSession" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "InlineFragment",
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "Node" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
],
},
},
],
},
},
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "node" }, name: { kind: "Name", value: "node" },
@@ -3434,6 +3465,7 @@ export const SessionDetailQueryDocument = {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
{ kind: "Field", name: { kind: "Name", value: "__typename" } }, { kind: "Field", name: { kind: "Name", value: "__typename" } },
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ {
kind: "FragmentSpread", kind: "FragmentSpread",
name: { kind: "Name", value: "CompatSession_detail" }, name: { kind: "Name", value: "CompatSession_detail" },
@@ -3604,7 +3636,7 @@ export const BrowserSessionListDocument = {
selections: [ selections: [
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "viewer" }, name: { kind: "Name", value: "viewerSession" },
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
@@ -3613,7 +3645,7 @@ export const BrowserSessionListDocument = {
kind: "InlineFragment", kind: "InlineFragment",
typeCondition: { typeCondition: {
kind: "NamedType", kind: "NamedType",
name: { kind: "Name", value: "User" }, name: { kind: "Name", value: "BrowserSession" },
}, },
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
@@ -3621,117 +3653,140 @@ export const BrowserSessionListDocument = {
{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "id" } },
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "browserSessions" }, name: { kind: "Name", value: "user" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: {
kind: "Variable",
name: { kind: "Name", value: "first" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "after" },
value: {
kind: "Variable",
name: { kind: "Name", value: "after" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "last" },
value: {
kind: "Variable",
name: { kind: "Name", value: "last" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "before" },
value: {
kind: "Variable",
name: { kind: "Name", value: "before" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "totalCount" }, name: { kind: "Name", value: "id" },
}, },
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "edges" }, name: { kind: "Name", value: "browserSessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: {
kind: "Variable",
name: { kind: "Name", value: "first" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "after" },
value: {
kind: "Variable",
name: { kind: "Name", value: "after" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "last" },
value: {
kind: "Variable",
name: { kind: "Name", value: "last" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "before" },
value: {
kind: "Variable",
name: { kind: "Name", value: "before" },
},
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "cursor" }, name: { kind: "Name", value: "totalCount" },
}, },
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "node" }, name: { kind: "Name", value: "edges" },
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
selections: [ selections: [
{ {
kind: "Field", kind: "Field",
name: { kind: "Name", value: "id" },
},
{
kind: "FragmentSpread",
name: { name: {
kind: "Name", kind: "Name",
value: "BrowserSession_session", value: "cursor",
},
},
{
kind: "Field",
name: { kind: "Name", value: "node" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: {
kind: "Name",
value: "id",
},
},
{
kind: "FragmentSpread",
name: {
kind: "Name",
value:
"BrowserSession_session",
},
},
],
}, },
}, },
], ],
}, },
}, },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "pageInfo" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ {
kind: "Field", kind: "Field",
name: { name: { kind: "Name", value: "pageInfo" },
kind: "Name", selectionSet: {
value: "hasNextPage", kind: "SelectionSet",
selections: [
{
kind: "Field",
name: {
kind: "Name",
value: "hasNextPage",
},
},
{
kind: "Field",
name: {
kind: "Name",
value: "hasPreviousPage",
},
},
{
kind: "Field",
name: {
kind: "Name",
value: "startCursor",
},
},
{
kind: "Field",
name: {
kind: "Name",
value: "endCursor",
},
},
],
}, },
}, },
{
kind: "Field",
name: {
kind: "Name",
value: "hasPreviousPage",
},
},
{
kind: "Field",
name: {
kind: "Name",
value: "startCursor",
},
},
{
kind: "Field",
name: { kind: "Name", value: "endCursor" },
},
], ],
}, },
}, },

View File

@@ -33,6 +33,7 @@ const router = createRouter({
routeTree, routeTree,
basepath: config.root, basepath: config.root,
defaultErrorComponent: GenericError, defaultErrorComponent: GenericError,
defaultPreload: "intent",
context: { client }, context: { client },
}); });

View File

@@ -40,8 +40,15 @@ export const Route = createFileRoute("/_account/sessions/$id")({
const QUERY = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
query SessionDetailQuery($id: ID!) { query SessionDetailQuery($id: ID!) {
viewerSession {
... on Node {
id
}
}
node(id: $id) { node(id: $id) {
__typename __typename
id
...CompatSession_detail ...CompatSession_detail
...OAuth2Session_detail ...OAuth2Session_detail
...BrowserSession_detail ...BrowserSession_detail
@@ -72,6 +79,7 @@ function SessionDetail(): React.ReactElement {
if (result.error) throw result.error; if (result.error) throw result.error;
const node = result.data?.node; const node = result.data?.node;
if (!node) throw notFound(); if (!node) throw notFound();
const currentSessionId = result.data?.viewerSession?.id;
switch (node.__typename) { switch (node.__typename) {
case "CompatSession": case "CompatSession":
@@ -79,7 +87,12 @@ function SessionDetail(): React.ReactElement {
case "Oauth2Session": case "Oauth2Session":
return <OAuth2SessionDetail session={node} />; return <OAuth2SessionDetail session={node} />;
case "BrowserSession": case "BrowserSession":
return <BrowserSessionDetail session={node} />; return (
<BrowserSessionDetail
session={node}
isCurrent={node.id === currentSessionId}
/>
);
default: default:
throw new Error("Unknown session type"); throw new Error("Unknown session type");
} }

View File

@@ -37,32 +37,37 @@ const QUERY = graphql(/* GraphQL */ `
$last: Int $last: Int
$before: String $before: String
) { ) {
viewer { viewerSession {
__typename __typename
... on User { ... on BrowserSession {
id id
browserSessions(
first: $first
after: $after
last: $last
before: $before
state: ACTIVE
) {
totalCount
edges { user {
cursor id
node {
id browserSessions(
...BrowserSession_session first: $first
after: $after
last: $last
before: $before
state: ACTIVE
) {
totalCount
edges {
cursor
node {
id
...BrowserSession_session
}
} }
}
pageInfo { pageInfo {
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
startCursor startCursor
endCursor endCursor
}
} }
} }
} }
@@ -84,7 +89,8 @@ export const Route = createFileRoute("/_account/sessions/browsers")({
fetchOptions: { signal }, fetchOptions: { signal },
}); });
if (result.error) throw result.error; if (result.error) throw result.error;
if (result.data?.viewer?.__typename !== "User") throw notFound(); if (result.data?.viewerSession?.__typename !== "BrowserSession")
throw notFound();
}, },
component: BrowserSessions, component: BrowserSessions,
@@ -95,26 +101,30 @@ function BrowserSessions(): React.ReactElement {
const pagination = Route.useLoaderDeps(); const pagination = Route.useLoaderDeps();
const [list] = useQuery({ query: QUERY, variables: pagination }); const [list] = useQuery({ query: QUERY, variables: pagination });
if (list.error) throw list.error; if (list.error) throw list.error;
const browserSessions = const currentSession =
list.data?.viewer.__typename === "User" list.data?.viewerSession.__typename === "BrowserSession"
? list.data.viewer.browserSessions ? list.data.viewerSession
: null; : null;
if (browserSessions === null) throw notFound(); if (currentSession === null) throw notFound();
const [backwardPage, forwardPage] = usePages( const [backwardPage, forwardPage] = usePages(
pagination, pagination,
browserSessions.pageInfo, currentSession.user.browserSessions.pageInfo,
PAGE_SIZE, PAGE_SIZE,
); );
// We reverse the list as we are paginating backwards // We reverse the list as we are paginating backwards
const edges = [...browserSessions.edges].reverse(); const edges = [...currentSession.user.browserSessions.edges].reverse();
return ( return (
<BlockList> <BlockList>
<H5>{t("frontend.browser_sessions_overview.heading")}</H5> <H5>{t("frontend.browser_sessions_overview.heading")}</H5>
{edges.map((n) => ( {edges.map((n) => (
<BrowserSession key={n.cursor} session={n.node} /> <BrowserSession
key={n.cursor}
session={n.node}
isCurrent={currentSession.id === n.node.id}
/>
))} ))}
<div className="flex *:flex-1"> <div className="flex *:flex-1">