mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-06-06 19:52:17 +00:00
Frontend cleanups
Mainly: - better handling of GraphQL errors - better logout state - dependencies update - a way to end browser sessions in the GraphQL API
This commit is contained in:
+54
-13
@@ -12,13 +12,52 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { atom } from "jotai";
|
||||
import { AnyVariables, CombinedError, OperationContext } from "@urql/core";
|
||||
import { atom, WritableAtom } from "jotai";
|
||||
import { useHydrateAtoms } from "jotai/utils";
|
||||
import { atomWithQuery, clientAtom } from "jotai-urql";
|
||||
import { AtomWithQuery, atomWithQuery, clientAtom } from "jotai-urql";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { graphql } from "./gql";
|
||||
import { client } from "./graphql";
|
||||
import { err, ok, Result } from "./result";
|
||||
|
||||
export type GqlResult<T> = Result<T, CombinedError>;
|
||||
export type GqlAtom<T> = WritableAtom<
|
||||
Promise<GqlResult<T>>,
|
||||
[context?: Partial<OperationContext>],
|
||||
void
|
||||
>;
|
||||
|
||||
/**
|
||||
* Map the result of a query atom to a new value, making it a GqlResult
|
||||
*
|
||||
* @param queryAtom: An atom got from atomWithQuery
|
||||
* @param mapper: A function that takes the data from the query and returns a new value
|
||||
*/
|
||||
export const mapQueryAtom = <Data, Variables extends AnyVariables, NewData>(
|
||||
queryAtom: AtomWithQuery<Data, Variables>,
|
||||
mapper: (data: Data) => NewData
|
||||
): GqlAtom<NewData> => {
|
||||
return atom(
|
||||
async (get): Promise<GqlResult<NewData>> => {
|
||||
const result = await get(queryAtom);
|
||||
if (result.error) {
|
||||
return err(result.error);
|
||||
}
|
||||
|
||||
if (result.data === undefined) {
|
||||
throw new Error("Query result is undefined");
|
||||
}
|
||||
|
||||
return ok(mapper(result.data));
|
||||
},
|
||||
|
||||
(_get, set, context) => {
|
||||
set(queryAtom, context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const HydrateAtoms: React.FC<{ children: ReactElement }> = ({
|
||||
children,
|
||||
@@ -44,13 +83,15 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
|
||||
const currentViewerAtom = atomWithQuery({ query: CURRENT_VIEWER_QUERY });
|
||||
|
||||
export const currentUserIdAtom = atom(async (get) => {
|
||||
const result = await get(currentViewerAtom);
|
||||
if (result.data?.viewer.__typename === "User") {
|
||||
return result.data.viewer.id;
|
||||
export const currentUserIdAtom: GqlAtom<string | null> = mapQueryAtom(
|
||||
currentViewerAtom,
|
||||
(data) => {
|
||||
if (data.viewer.__typename === "User") {
|
||||
return data.viewer.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
);
|
||||
|
||||
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerSessionQuery {
|
||||
@@ -71,11 +112,11 @@ const currentViewerSessionAtom = atomWithQuery({
|
||||
query: CURRENT_VIEWER_SESSION_QUERY,
|
||||
});
|
||||
|
||||
export const currentBrowserSessionIdAtom = atom(
|
||||
async (get): Promise<string | null> => {
|
||||
const result = await get(currentViewerSessionAtom);
|
||||
if (result.data?.viewerSession.__typename === "BrowserSession") {
|
||||
return result.data.viewerSession.id;
|
||||
export const currentBrowserSessionIdAtom: GqlAtom<string | null> = mapQueryAtom(
|
||||
currentViewerSessionAtom,
|
||||
(data) => {
|
||||
if (data.viewerSession.__typename === "BrowserSession") {
|
||||
return data.viewerSession.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
import IconWebBrowser from "@vector-im/compound-design-tokens/icons/web-browser.svg";
|
||||
import { Body } from "@vector-im/compound-web";
|
||||
import { Body, Button } from "@vector-im/compound-web";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithMutation } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms";
|
||||
import { FragmentType, graphql, useFragment } from "../gql";
|
||||
|
||||
import Block from "./Block";
|
||||
@@ -31,6 +36,30 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const END_SESSION_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation EndBrowserSession($id: ID!) {
|
||||
endBrowserSession(input: { browserSessionId: $id }) {
|
||||
status
|
||||
browserSession {
|
||||
id
|
||||
...BrowserSession_session
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const endSessionFamily = atomFamily((id: string) => {
|
||||
const endSession = atomWithMutation(END_SESSION_MUTATION);
|
||||
|
||||
// A proxy atom which pre-sets the id variable in the mutation
|
||||
const endSessionAtom = atom(
|
||||
(get) => get(endSession),
|
||||
(get, set) => set(endSession, { id })
|
||||
);
|
||||
|
||||
return endSessionAtom;
|
||||
});
|
||||
|
||||
type Props = {
|
||||
session: FragmentType<typeof FRAGMENT>;
|
||||
isCurrent: boolean;
|
||||
@@ -38,10 +67,30 @@ type Props = {
|
||||
|
||||
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
const data = useFragment(FRAGMENT, session);
|
||||
const [pending, startTransition] = useTransition();
|
||||
const endSession = useSetAtom(endSessionFamily(data.id));
|
||||
|
||||
// Pull those atoms to reset them when the current session is ended
|
||||
const currentUserId = useSetAtom(currentUserIdAtom);
|
||||
const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom);
|
||||
|
||||
// const lastAuthentication = data.lastAuthentication?.createdAt;
|
||||
const createdAt = data.createdAt;
|
||||
|
||||
const onSessionEnd = () => {
|
||||
startTransition(() => {
|
||||
endSession().then(() => {
|
||||
if (isCurrent) {
|
||||
currentBrowserSessionId({
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
currentUserId({
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Block className="my-4 flex items-center">
|
||||
<IconWebBrowser className="mr-4 session-icon" />
|
||||
@@ -59,15 +108,16 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
Signed in <DateTime datetime={createdAt} />
|
||||
</Body>
|
||||
</div>
|
||||
<Body
|
||||
as="a"
|
||||
|
||||
<Button
|
||||
kind="destructive"
|
||||
size="sm"
|
||||
weight="medium"
|
||||
href="#"
|
||||
className="text-critical underline hover:no-underline"
|
||||
className="mt-2"
|
||||
onClick={onSessionEnd}
|
||||
disabled={pending}
|
||||
>
|
||||
Sign out
|
||||
</Body>
|
||||
</Button>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { currentBrowserSessionIdAtom } from "../atoms";
|
||||
import { currentBrowserSessionIdAtom, mapQueryAtom } from "../atoms";
|
||||
import { graphql } from "../gql";
|
||||
import { PageInfo } from "../gql/graphql";
|
||||
import {
|
||||
@@ -25,9 +25,11 @@ import {
|
||||
atomWithPagination,
|
||||
Pagination,
|
||||
} from "../pagination";
|
||||
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
import BlockList from "./BlockList";
|
||||
import BrowserSession from "./BrowserSession";
|
||||
import GraphQLError from "./GraphQLError";
|
||||
import PaginationControls from "./PaginationControls";
|
||||
import { Title } from "./Typography";
|
||||
|
||||
@@ -69,17 +71,23 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
const currentPaginationAtom = atomForCurrentPagination();
|
||||
|
||||
const browserSessionListFamily = atomFamily((userId: string) => {
|
||||
const browserSessionList = atomWithQuery({
|
||||
const browserSessionListQuery = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||
});
|
||||
|
||||
const browserSessionList = mapQueryAtom(
|
||||
browserSessionListQuery,
|
||||
(data) => data.user?.browserSessions || null
|
||||
);
|
||||
|
||||
return browserSessionList;
|
||||
});
|
||||
|
||||
const pageInfoFamily = atomFamily((userId: string) => {
|
||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
||||
const result = await get(browserSessionListFamily(userId));
|
||||
return result.data?.user?.browserSessions?.pageInfo ?? null;
|
||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
||||
});
|
||||
return pageInfoAtom;
|
||||
});
|
||||
@@ -94,40 +102,43 @@ const paginationFamily = atomFamily((userId: string) => {
|
||||
});
|
||||
|
||||
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const currentSessionId = useAtomValue(currentBrowserSessionIdAtom);
|
||||
const currentSessionIdResult = useAtomValue(currentBrowserSessionIdAtom);
|
||||
const [pending, startTransition] = useTransition();
|
||||
const result = useAtomValue(browserSessionListFamily(userId));
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
|
||||
if (isErr(currentSessionIdResult))
|
||||
return <GraphQLError error={unwrapErr(currentSessionIdResult)} />;
|
||||
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(() => {
|
||||
setPagination(pagination);
|
||||
});
|
||||
};
|
||||
|
||||
if (result.data?.user?.browserSessions) {
|
||||
const data = result.data.user.browserSessions;
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of browser sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of browser sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
/>
|
||||
{browserSessions.edges.map((n) => (
|
||||
<BrowserSession
|
||||
key={n.cursor}
|
||||
session={n.node}
|
||||
isCurrent={n.node.id === currentSessionId}
|
||||
/>
|
||||
{data.edges.map((n) => (
|
||||
<BrowserSession
|
||||
key={n.cursor}
|
||||
session={n.node}
|
||||
isCurrent={n.node.id === currentSessionId}
|
||||
/>
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
}
|
||||
|
||||
return <>Failed to load browser sessions</>;
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowserSessionList;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import { graphql } from "../gql";
|
||||
import { PageInfo } from "../gql/graphql";
|
||||
import {
|
||||
@@ -24,9 +25,11 @@ import {
|
||||
atomWithPagination,
|
||||
Pagination,
|
||||
} from "../pagination";
|
||||
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
import BlockList from "./BlockList";
|
||||
import CompatSsoLogin from "./CompatSsoLogin";
|
||||
import GraphQLError from "./GraphQLError";
|
||||
import PaginationControls from "./PaginationControls";
|
||||
import { Title } from "./Typography";
|
||||
|
||||
@@ -67,18 +70,23 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
const currentPaginationAtom = atomForCurrentPagination();
|
||||
|
||||
const compatSsoLoginListFamily = atomFamily((userId: string) => {
|
||||
const compatSsoLoginList = atomWithQuery({
|
||||
const compatSsoLoginListQuery = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||
});
|
||||
|
||||
const compatSsoLoginList = mapQueryAtom(
|
||||
compatSsoLoginListQuery,
|
||||
(data) => data.user?.compatSsoLogins || null
|
||||
);
|
||||
|
||||
return compatSsoLoginList;
|
||||
});
|
||||
|
||||
const pageInfoFamily = atomFamily((userId: string) => {
|
||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
||||
const result = await get(compatSsoLoginListFamily(userId));
|
||||
return result.data?.user?.compatSsoLogins?.pageInfo ?? null;
|
||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
||||
});
|
||||
|
||||
return pageInfoAtom;
|
||||
@@ -98,30 +106,30 @@ const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
const compatSsoLoginList = unwrapOk(result);
|
||||
if (compatSsoLoginList === null)
|
||||
return <>Failed to load list of compatibility sessions.</>;
|
||||
|
||||
const paginate = (pagination: Pagination): void => {
|
||||
startTransition(() => {
|
||||
setPagination(pagination);
|
||||
});
|
||||
};
|
||||
|
||||
if (result.data?.user?.compatSsoLogins) {
|
||||
const data = result.data.user.compatSsoLogins;
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of compatibility sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
/>
|
||||
{data.edges.map((n) => (
|
||||
<CompatSsoLogin login={n.node} key={n.node.id} />
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
}
|
||||
|
||||
return <>Failed to load list of compatibility sessions.</>;
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of compatibility sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
/>
|
||||
{compatSsoLoginList.edges.map((n) => (
|
||||
<CompatSsoLogin login={n.node} key={n.node.id} />
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompatSsoLoginList;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 { CombinedError } from "@urql/core";
|
||||
import { Alert } from "@vector-im/compound-web";
|
||||
|
||||
const GraphQLError: React.FC<{ error: CombinedError }> = ({ error }) => (
|
||||
<Alert type="critical" title={error.message}>
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
export default GraphQLError;
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 { Alert } from "@vector-im/compound-web";
|
||||
|
||||
const NotFound: React.FC = () => <Alert type="critical" title="Not found." />;
|
||||
|
||||
export default NotFound;
|
||||
@@ -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.
|
||||
|
||||
import { Alert } from "@vector-im/compound-web";
|
||||
|
||||
const NotLoggedIn: React.FC = () => (
|
||||
<Alert type="critical" title="You're not logged in." />
|
||||
);
|
||||
|
||||
export default NotLoggedIn;
|
||||
@@ -41,7 +41,7 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
||||
`);
|
||||
|
||||
const END_SESSION_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation EndSession($id: ID!) {
|
||||
mutation EndOAuth2Session($id: ID!) {
|
||||
endOauth2Session(input: { oauth2SessionId: $id }) {
|
||||
status
|
||||
oauth2Session {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import { graphql } from "../gql";
|
||||
import { PageInfo } from "../gql/graphql";
|
||||
import {
|
||||
@@ -24,8 +25,10 @@ import {
|
||||
atomWithPagination,
|
||||
Pagination,
|
||||
} from "../pagination";
|
||||
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
import BlockList from "./BlockList";
|
||||
import GraphQLError from "./GraphQLError";
|
||||
import OAuth2Session from "./OAuth2Session";
|
||||
import PaginationControls from "./PaginationControls";
|
||||
import { Title } from "./Typography";
|
||||
@@ -68,18 +71,23 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
const currentPaginationAtom = atomForCurrentPagination();
|
||||
|
||||
const oauth2SessionListFamily = atomFamily((userId: string) => {
|
||||
const oauth2SessionList = atomWithQuery({
|
||||
const oauth2SessionListQuery = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||
});
|
||||
|
||||
const oauth2SessionList = mapQueryAtom(
|
||||
oauth2SessionListQuery,
|
||||
(data) => data.user?.oauth2Sessions || null
|
||||
);
|
||||
|
||||
return oauth2SessionList;
|
||||
});
|
||||
|
||||
const pageInfoFamily = atomFamily((userId: string) => {
|
||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
||||
const result = await get(oauth2SessionListFamily(userId));
|
||||
return result.data?.user?.oauth2Sessions?.pageInfo ?? null;
|
||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
||||
});
|
||||
|
||||
return pageInfoAtom;
|
||||
@@ -103,30 +111,30 @@ const OAuth2SessionList: React.FC<Props> = ({ userId }) => {
|
||||
const setPagination = useSetAtom(currentPaginationAtom);
|
||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
const oauth2Sessions = unwrapOk(result);
|
||||
if (oauth2Sessions === null)
|
||||
return <>Failed to load OAuth 2.0 session list</>;
|
||||
|
||||
const paginate = (pagination: Pagination): void => {
|
||||
startTransition(() => {
|
||||
setPagination(pagination);
|
||||
});
|
||||
};
|
||||
|
||||
if (result.data?.user?.oauth2Sessions) {
|
||||
const data = result.data.user.oauth2Sessions;
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of OAuth 2.0 sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
/>
|
||||
{data.edges.map((n) => (
|
||||
<OAuth2Session key={n.cursor} session={n.node} />
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
} else {
|
||||
return <>Failed to load OAuth 2.0 session list</>;
|
||||
}
|
||||
return (
|
||||
<BlockList>
|
||||
<Title>List of OAuth 2.0 sessions:</Title>
|
||||
<PaginationControls
|
||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||
disabled={pending}
|
||||
/>
|
||||
{oauth2Sessions.edges.map((n) => (
|
||||
<OAuth2Session key={n.cursor} session={n.node} />
|
||||
))}
|
||||
</BlockList>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2SessionList;
|
||||
|
||||
+12
-4
@@ -21,6 +21,8 @@ const documents = {
|
||||
types.AddEmailDocument,
|
||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\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 query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\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":
|
||||
types.BrowserSessionListDocument,
|
||||
"\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n ...CompatSsoLogin_session\n createdAt\n deviceId\n finishedAt\n }\n }\n":
|
||||
@@ -33,8 +35,8 @@ const documents = {
|
||||
types.CompatSsoLoginListDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n":
|
||||
types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
||||
types.EndSessionDocument,
|
||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
||||
types.EndOAuth2SessionDocument,
|
||||
"\n query OAuth2SessionListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
|
||||
types.OAuth2SessionListQueryDocument,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n":
|
||||
@@ -97,6 +99,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"
|
||||
): typeof documents["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
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 documents["\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -137,8 +145,8 @@ export function graphql(
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "\n mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"
|
||||
): typeof documents["\n mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"];
|
||||
source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"
|
||||
): typeof documents["\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
+152
-5
@@ -184,6 +184,28 @@ export type CreationEvent = {
|
||||
createdAt: Scalars["DateTime"]["output"];
|
||||
};
|
||||
|
||||
/** The input of the `endBrowserSession` mutation. */
|
||||
export type EndBrowserSessionInput = {
|
||||
/** The ID of the session to end. */
|
||||
browserSessionId: Scalars["ID"]["input"];
|
||||
};
|
||||
|
||||
export type EndBrowserSessionPayload = {
|
||||
__typename?: "EndBrowserSessionPayload";
|
||||
/** Returns the ended session. */
|
||||
browserSession?: Maybe<BrowserSession>;
|
||||
/** The status of the mutation. */
|
||||
status: EndBrowserSessionStatus;
|
||||
};
|
||||
|
||||
/** The status of the `endBrowserSession` mutation. */
|
||||
export enum EndBrowserSessionStatus {
|
||||
/** The session was ended. */
|
||||
Ended = "ENDED",
|
||||
/** The session was not found. */
|
||||
NotFound = "NOT_FOUND",
|
||||
}
|
||||
|
||||
/** The input of the `endCompatSession` mutation. */
|
||||
export type EndCompatSessionInput = {
|
||||
/** The ID of the session to end. */
|
||||
@@ -243,6 +265,7 @@ export type Mutation = {
|
||||
__typename?: "Mutation";
|
||||
/** Add an email address to the specified user */
|
||||
addEmail: AddEmailPayload;
|
||||
endBrowserSession: EndBrowserSessionPayload;
|
||||
endCompatSession: EndCompatSessionPayload;
|
||||
endOauth2Session: EndOAuth2SessionPayload;
|
||||
/** Remove an email address */
|
||||
@@ -260,6 +283,11 @@ export type MutationAddEmailArgs = {
|
||||
input: AddEmailInput;
|
||||
};
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationEndBrowserSessionArgs = {
|
||||
input: EndBrowserSessionInput;
|
||||
};
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationEndCompatSessionArgs = {
|
||||
input: EndCompatSessionInput;
|
||||
@@ -777,6 +805,25 @@ export type BrowserSession_SessionFragment = {
|
||||
} | null;
|
||||
} & { " $fragmentName"?: "BrowserSession_SessionFragment" };
|
||||
|
||||
export type EndBrowserSessionMutationVariables = Exact<{
|
||||
id: Scalars["ID"]["input"];
|
||||
}>;
|
||||
|
||||
export type EndBrowserSessionMutation = {
|
||||
__typename?: "Mutation";
|
||||
endBrowserSession: {
|
||||
__typename?: "EndBrowserSessionPayload";
|
||||
status: EndBrowserSessionStatus;
|
||||
browserSession?:
|
||||
| ({ __typename?: "BrowserSession"; id: string } & {
|
||||
" $fragmentRefs"?: {
|
||||
BrowserSession_SessionFragment: BrowserSession_SessionFragment;
|
||||
};
|
||||
})
|
||||
| null;
|
||||
};
|
||||
};
|
||||
|
||||
export type BrowserSessionListQueryVariables = Exact<{
|
||||
userId: Scalars["ID"]["input"];
|
||||
first?: InputMaybe<Scalars["Int"]["input"]>;
|
||||
@@ -908,11 +955,11 @@ export type OAuth2Session_SessionFragment = {
|
||||
};
|
||||
} & { " $fragmentName"?: "OAuth2Session_SessionFragment" };
|
||||
|
||||
export type EndSessionMutationVariables = Exact<{
|
||||
export type EndOAuth2SessionMutationVariables = Exact<{
|
||||
id: Scalars["ID"]["input"];
|
||||
}>;
|
||||
|
||||
export type EndSessionMutation = {
|
||||
export type EndOAuth2SessionMutation = {
|
||||
__typename?: "Mutation";
|
||||
endOauth2Session: {
|
||||
__typename?: "EndOAuth2SessionPayload";
|
||||
@@ -1532,6 +1579,103 @@ export const AddEmailDocument = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<AddEmailMutation, AddEmailMutationVariables>;
|
||||
export const EndBrowserSessionDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "mutation",
|
||||
name: { kind: "Name", value: "EndBrowserSession" },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: "VariableDefinition",
|
||||
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
|
||||
type: {
|
||||
kind: "NonNullType",
|
||||
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "endBrowserSession" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "input" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "browserSessionId" },
|
||||
value: {
|
||||
kind: "Variable",
|
||||
name: { kind: "Name", value: "id" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "status" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "browserSession" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "BrowserSession_session" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "BrowserSession_session" },
|
||||
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: "lastAuthentication" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
EndBrowserSessionMutation,
|
||||
EndBrowserSessionMutationVariables
|
||||
>;
|
||||
export const BrowserSessionListDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -2054,13 +2198,13 @@ export const CompatSsoLoginListDocument = {
|
||||
CompatSsoLoginListQuery,
|
||||
CompatSsoLoginListQueryVariables
|
||||
>;
|
||||
export const EndSessionDocument = {
|
||||
export const EndOAuth2SessionDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "mutation",
|
||||
name: { kind: "Name", value: "EndSession" },
|
||||
name: { kind: "Name", value: "EndOAuth2Session" },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: "VariableDefinition",
|
||||
@@ -2151,7 +2295,10 @@ export const EndSessionDocument = {
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<EndSessionMutation, EndSessionMutationVariables>;
|
||||
} as unknown as DocumentNode<
|
||||
EndOAuth2SessionMutation,
|
||||
EndOAuth2SessionMutationVariables
|
||||
>;
|
||||
export const OAuth2SessionListQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
|
||||
@@ -522,6 +522,33 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "OBJECT",
|
||||
name: "EndBrowserSessionPayload",
|
||||
fields: [
|
||||
{
|
||||
name: "browserSession",
|
||||
type: {
|
||||
kind: "OBJECT",
|
||||
name: "BrowserSession",
|
||||
ofType: null,
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "SCALAR",
|
||||
name: "Any",
|
||||
},
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
interfaces: [],
|
||||
},
|
||||
{
|
||||
kind: "OBJECT",
|
||||
name: "EndCompatSessionPayload",
|
||||
@@ -637,6 +664,29 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "endBrowserSession",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "OBJECT",
|
||||
name: "EndBrowserSessionPayload",
|
||||
ofType: null,
|
||||
},
|
||||
},
|
||||
args: [
|
||||
{
|
||||
name: "input",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "SCALAR",
|
||||
name: "Any",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "endCompatSession",
|
||||
type: {
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import UserEmailList from "../components/UserEmailList";
|
||||
import UserGreeting from "../components/UserGreeting";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const UserAccount: React.FC<{ id: string }> = ({ id }) => {
|
||||
return (
|
||||
@@ -28,16 +31,17 @@ const UserAccount: React.FC<{ id: string }> = ({ id }) => {
|
||||
};
|
||||
|
||||
const CurrentUserAccount: React.FC = () => {
|
||||
const userId = useAtomValue(currentUserIdAtom);
|
||||
if (userId !== null) {
|
||||
return (
|
||||
<div className="w-96 mx-auto">
|
||||
<UserAccount id={userId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
return <div className="w-96 mx-auto">Not logged in.</div>;
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
<div className="w-96 mx-auto">
|
||||
<UserAccount id={userId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentUserAccount;
|
||||
|
||||
@@ -16,7 +16,11 @@ import { useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query BrowserSessionQuery($id: ID!) {
|
||||
@@ -36,26 +40,31 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
`);
|
||||
|
||||
const browserSessionFamily = atomFamily((id: string) => {
|
||||
const browserSessionAtom = atomWithQuery({
|
||||
const browserSessionQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: () => ({ id }),
|
||||
});
|
||||
|
||||
const browserSessionAtom = mapQueryAtom(
|
||||
browserSessionQueryAtom,
|
||||
(data) => data?.browserSession
|
||||
);
|
||||
|
||||
return browserSessionAtom;
|
||||
});
|
||||
|
||||
const BrowserSession: React.FC<{ id: string }> = ({ id }) => {
|
||||
const result = useAtomValue(browserSessionFamily(id));
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
if (result.data?.browserSession) {
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(result.data.browserSession, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
const browserSession = unwrapOk(result);
|
||||
if (browserSession === null) return <NotFound />;
|
||||
|
||||
return <>Failed to load browser session</>;
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(browserSession, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowserSession;
|
||||
|
||||
+18
-16
@@ -12,32 +12,34 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Alert } from "@vector-im/compound-web";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import BrowserSessionList from "../components/BrowserSessionList";
|
||||
import CompatSsoLoginList from "../components/CompatSsoLoginList";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import OAuth2SessionList from "../components/OAuth2SessionList";
|
||||
import UserGreeting from "../components/UserGreeting";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const currentUserId = useAtomValue(currentUserIdAtom);
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
if (currentUserId) {
|
||||
return (
|
||||
<>
|
||||
<UserGreeting userId={currentUserId} />
|
||||
<div className="mt-4 grid gap-1">
|
||||
<OAuth2SessionList userId={currentUserId} />
|
||||
<CompatSsoLoginList userId={currentUserId} />
|
||||
<BrowserSessionList userId={currentUserId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <Alert type="critical" title="You're not logged in." />;
|
||||
}
|
||||
const currentUserId = unwrapOk(result);
|
||||
if (currentUserId === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserGreeting userId={currentUserId} />
|
||||
<div className="mt-4 grid gap-1">
|
||||
<OAuth2SessionList userId={currentUserId} />
|
||||
<CompatSsoLoginList userId={currentUserId} />
|
||||
<BrowserSessionList userId={currentUserId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -16,7 +16,11 @@ import { useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query OAuth2ClientQuery($id: ID!) {
|
||||
@@ -33,26 +37,31 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
`);
|
||||
|
||||
const oauth2ClientFamily = atomFamily((id: string) => {
|
||||
const oauth2ClientAtom = atomWithQuery({
|
||||
const oauth2ClientQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: () => ({ id }),
|
||||
});
|
||||
|
||||
const oauth2ClientAtom = mapQueryAtom(
|
||||
oauth2ClientQueryAtom,
|
||||
(data) => data?.oauth2Client
|
||||
);
|
||||
|
||||
return oauth2ClientAtom;
|
||||
});
|
||||
|
||||
const OAuth2Client: React.FC<{ id: string }> = ({ id }) => {
|
||||
const result = useAtomValue(oauth2ClientFamily(id));
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
if (result.data?.oauth2Client) {
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(result.data.oauth2Client, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
const oauth2Client = unwrapOk(result);
|
||||
if (oauth2Client === null) return <NotFound />;
|
||||
|
||||
return <>Failed to load OAuth2 client</>;
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(oauth2Client, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2Client;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
const RESULT = Symbol("Result");
|
||||
const ERR = Symbol("Err");
|
||||
const OK = Symbol("Ok");
|
||||
|
||||
/**
|
||||
* An `Ok` is a type that represents a successful result.
|
||||
*/
|
||||
export type Ok<T> = {
|
||||
[RESULT]: typeof OK;
|
||||
[OK]: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* An `Err` is a type that represents an error.
|
||||
*/
|
||||
export type Err<E> = {
|
||||
[RESULT]: typeof ERR;
|
||||
[ERR]: E;
|
||||
};
|
||||
|
||||
/**
|
||||
* A `Result` is a type that represents either an `Ok` or an `Err`.
|
||||
*/
|
||||
export type Result<T, E> = Ok<T> | Err<E>;
|
||||
|
||||
// Construct an `Ok`
|
||||
export const ok = <T>(data: T): Ok<T> => ({ [RESULT]: OK, [OK]: data });
|
||||
|
||||
// Construct an `Err`
|
||||
export const err = <E>(error: E): Err<E> => ({
|
||||
[RESULT]: ERR,
|
||||
[ERR]: error,
|
||||
});
|
||||
|
||||
// Check if a `Result` is an `Ok`
|
||||
export const isOk = <T, E>(result: Result<T, E>): result is Ok<T> =>
|
||||
result[RESULT] === OK;
|
||||
|
||||
// Check if a `Result` is an `Err`
|
||||
export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
|
||||
result[RESULT] === ERR;
|
||||
|
||||
// Extract the data from an `Ok`
|
||||
export const unwrapOk = <T>(result: Ok<T>): T => result[OK];
|
||||
|
||||
// Extract the error from an `Err`
|
||||
export const unwrapErr = <E>(result: Err<E>): E => result[ERR];
|
||||
Reference in New Issue
Block a user