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
link to client detail, design pass on client detail page
This commit is contained in:
committed by
Quentin Gliech
parent
5e76adb325
commit
bf13e58d16
22
frontend/src/components/Client/OAuth2ClientDetail.module.css
Normal file
22
frontend/src/components/Client/OAuth2ClientDetail.module.css
Normal file
@@ -0,0 +1,22 @@
|
||||
/* 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: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-2x);
|
||||
}
|
||||
84
frontend/src/components/Client/OAuth2ClientDetail.tsx
Normal file
84
frontend/src/components/Client/OAuth2ClientDetail.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 } from "@vector-im/compound-web";
|
||||
|
||||
import { 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";
|
||||
|
||||
export const OAUTH2_CLIENT_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment OAuth2Client_detail on Oauth2Client {
|
||||
id
|
||||
clientId
|
||||
clientName
|
||||
clientUri
|
||||
logoUri
|
||||
tosUri
|
||||
policyUri
|
||||
redirectUris
|
||||
}
|
||||
`);
|
||||
|
||||
type Props = {
|
||||
client: FragmentType<typeof OAUTH2_CLIENT_FRAGMENT>;
|
||||
};
|
||||
|
||||
const FriendlyExternalLink: React.FC<{ uri?: string }> = ({ uri }) => {
|
||||
if (!uri) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(uri);
|
||||
const friendlyUrl = url.host + url.pathname;
|
||||
|
||||
return <ExternalLink href={uri}>{friendlyUrl}</ExternalLink>;
|
||||
};
|
||||
|
||||
const OAuth2ClientDetail: React.FC<Props> = ({ client }) => {
|
||||
const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client);
|
||||
|
||||
const details = [
|
||||
{ label: "Name", value: data.clientName },
|
||||
{ label: "Client ID", value: <code>{data.clientId}</code> },
|
||||
{
|
||||
label: "Terms of service",
|
||||
value: data.tosUri && <FriendlyExternalLink uri={data.tosUri} />,
|
||||
},
|
||||
{
|
||||
label: "Policy",
|
||||
value: data.policyUri && <FriendlyExternalLink uri={data.policyUri} />,
|
||||
},
|
||||
].filter(({ value }) => !!value);
|
||||
|
||||
return (
|
||||
<BlockList>
|
||||
<header className={styles.header}>
|
||||
<ClientAvatar
|
||||
logoUri={data.logoUri}
|
||||
name={data.clientName || data.clientId}
|
||||
size="1.5rem"
|
||||
/>
|
||||
<H3>{data.clientName}</H3>
|
||||
</header>
|
||||
<SessionDetails title="Client" details={details} />
|
||||
</BlockList>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientDetail;
|
||||
19
frontend/src/components/ExternalLink/ExternalLink.module.css
Normal file
19
frontend/src/components/ExternalLink/ExternalLink.module.css
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
.external-link {
|
||||
/* override compound style */
|
||||
color: var(--cpd-color-text-link-external) !important;
|
||||
}
|
||||
34
frontend/src/components/ExternalLink/ExternalLink.tsx
Normal file
34
frontend/src/components/ExternalLink/ExternalLink.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 { Link } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./ExternalLink.module.css";
|
||||
|
||||
const ExternalLink: React.FC<React.ComponentProps<typeof Link>> = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<Link
|
||||
className={classNames(className, styles.externalLink)}
|
||||
target="_blank"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default ExternalLink;
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
simplifyUrl,
|
||||
} from "../CompatSession";
|
||||
import DateTime from "../DateTime";
|
||||
import ExternalLink from "../ExternalLink/ExternalLink";
|
||||
import EndSessionButton from "../Session/EndSessionButton";
|
||||
|
||||
import SessionDetails from "./SessionDetails";
|
||||
@@ -59,9 +60,9 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
|
||||
clientDetails.push({
|
||||
label: "Uri",
|
||||
value: (
|
||||
<a target="_blank" href={data.ssoLogin?.redirectUri}>
|
||||
<ExternalLink target="_blank" href={data.ssoLogin?.redirectUri}>
|
||||
{data.ssoLogin?.redirectUri}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { H3 } from "@vector-im/compound-web";
|
||||
import { useSetAtom } from "jotai";
|
||||
|
||||
import { FragmentType, useFragment } from "../../gql";
|
||||
import { Link } from "../../routing";
|
||||
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
|
||||
import BlockList from "../BlockList/BlockList";
|
||||
import DateTime from "../DateTime";
|
||||
@@ -70,6 +71,9 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
|
||||
},
|
||||
];
|
||||
|
||||
const clientTitle = (
|
||||
<Link route={{ type: "client", id: data.client.id }}>Client</Link>
|
||||
);
|
||||
const clientDetails = [
|
||||
{
|
||||
label: "Name",
|
||||
@@ -100,7 +104,7 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
|
||||
<BlockList>
|
||||
<H3>{deviceId || data.id}</H3>
|
||||
<SessionDetails title="Session" details={sessionDetails} />
|
||||
<SessionDetails title="Client" details={clientDetails} />
|
||||
<SessionDetails title={clientTitle} details={clientDetails} />
|
||||
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ import styles from "./SessionDetails.module.css";
|
||||
|
||||
type Detail = { label: string; value: string | ReactNode };
|
||||
type Props = {
|
||||
title: string;
|
||||
title: string | ReactNode;
|
||||
details: Detail[];
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ const documents = {
|
||||
types.EndBrowserSessionDocument,
|
||||
"\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\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 state: $state\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":
|
||||
types.BrowserSessionListDocument,
|
||||
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
|
||||
types.OAuth2Client_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
||||
types.CompatSession_SessionFragmentDoc,
|
||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
||||
@@ -65,7 +67,7 @@ const documents = {
|
||||
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":
|
||||
types.BrowserSessionQueryDocument,
|
||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n":
|
||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
|
||||
types.OAuth2ClientQueryDocument,
|
||||
"\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserSessionsOverview_user\n }\n }\n }\n":
|
||||
types.SessionsOverviewQueryDocument,
|
||||
@@ -117,6 +119,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
source: "\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\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 state: $state\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",
|
||||
): (typeof documents)["\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\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 state: $state\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"];
|
||||
/**
|
||||
* 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 documents)["\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -247,8 +255,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 query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n",
|
||||
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n"];
|
||||
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n",
|
||||
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1083,6 +1083,18 @@ export type BrowserSessionListQuery = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type OAuth2Client_DetailFragment = {
|
||||
__typename?: "Oauth2Client";
|
||||
id: string;
|
||||
clientId: string;
|
||||
clientName?: string | null;
|
||||
clientUri?: any | null;
|
||||
logoUri?: any | null;
|
||||
tosUri?: any | null;
|
||||
policyUri?: any | null;
|
||||
redirectUris: Array<any>;
|
||||
} & { " $fragmentName"?: "OAuth2Client_DetailFragment" };
|
||||
|
||||
export type CompatSession_SessionFragment = {
|
||||
__typename?: "CompatSession";
|
||||
id: string;
|
||||
@@ -1497,17 +1509,13 @@ export type OAuth2ClientQueryQueryVariables = Exact<{
|
||||
|
||||
export type OAuth2ClientQueryQuery = {
|
||||
__typename?: "Query";
|
||||
oauth2Client?: {
|
||||
__typename?: "Oauth2Client";
|
||||
id: string;
|
||||
clientId: string;
|
||||
clientName?: string | null;
|
||||
clientUri?: any | null;
|
||||
tosUri?: any | null;
|
||||
policyUri?: any | null;
|
||||
redirectUris: Array<any>;
|
||||
logoUri?: any | null;
|
||||
} | null;
|
||||
oauth2Client?:
|
||||
| ({ __typename?: "Oauth2Client" } & {
|
||||
" $fragmentRefs"?: {
|
||||
OAuth2Client_DetailFragment: OAuth2Client_DetailFragment;
|
||||
};
|
||||
})
|
||||
| null;
|
||||
};
|
||||
|
||||
export type SessionsOverviewQueryQueryVariables = Exact<{
|
||||
@@ -1573,6 +1581,32 @@ export const BrowserSession_SessionFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
|
||||
export const OAuth2Client_DetailFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "OAuth2Client_detail" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "Oauth2Client" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<OAuth2Client_DetailFragment, unknown>;
|
||||
export const CompatSession_SessionFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -4166,23 +4200,37 @@ export const OAuth2ClientQueryDocument = {
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "redirectUris" },
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "OAuth2Client_detail" },
|
||||
},
|
||||
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "OAuth2Client_detail" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "Oauth2Client" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
OAuth2ClientQueryQuery,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import { graphql } from "../gql";
|
||||
@@ -25,14 +26,7 @@ import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query OAuth2ClientQuery($id: ID!) {
|
||||
oauth2Client(id: $id) {
|
||||
id
|
||||
clientId
|
||||
clientName
|
||||
clientUri
|
||||
tosUri
|
||||
policyUri
|
||||
redirectUris
|
||||
logoUri
|
||||
...OAuth2Client_detail
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -56,13 +50,9 @@ const OAuth2Client: React.FC<{ id: string }> = ({ id }) => {
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const oauth2Client = unwrapOk(result);
|
||||
if (oauth2Client === null) return <NotFound />;
|
||||
if (!oauth2Client) return <NotFound />;
|
||||
|
||||
return (
|
||||
<pre>
|
||||
<code>{JSON.stringify(oauth2Client, null, 2)}</code>
|
||||
</pre>
|
||||
);
|
||||
return <OAuth2ClientDetail client={oauth2Client} />;
|
||||
};
|
||||
|
||||
export default OAuth2Client;
|
||||
|
||||
Reference in New Issue
Block a user