diff --git a/frontend/src/components/Client/OAuth2ClientDetail.module.css b/frontend/src/components/Client/OAuth2ClientDetail.module.css new file mode 100644 index 00000000..a15e7706 --- /dev/null +++ b/frontend/src/components/Client/OAuth2ClientDetail.module.css @@ -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); +} diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx new file mode 100644 index 00000000..62d29307 --- /dev/null +++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx @@ -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; +}; + +const FriendlyExternalLink: React.FC<{ uri?: string }> = ({ uri }) => { + if (!uri) { + return null; + } + const url = new URL(uri); + const friendlyUrl = url.host + url.pathname; + + return {friendlyUrl}; +}; + +const OAuth2ClientDetail: React.FC = ({ client }) => { + const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client); + + const details = [ + { label: "Name", value: data.clientName }, + { label: "Client ID", value: {data.clientId} }, + { + label: "Terms of service", + value: data.tosUri && , + }, + { + label: "Policy", + value: data.policyUri && , + }, + ].filter(({ value }) => !!value); + + return ( + +
+ +

{data.clientName}

+
+ +
+ ); +}; + +export default OAuth2ClientDetail; diff --git a/frontend/src/components/ExternalLink/ExternalLink.module.css b/frontend/src/components/ExternalLink/ExternalLink.module.css new file mode 100644 index 00000000..e4a0f52b --- /dev/null +++ b/frontend/src/components/ExternalLink/ExternalLink.module.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/ExternalLink/ExternalLink.tsx b/frontend/src/components/ExternalLink/ExternalLink.tsx new file mode 100644 index 00000000..b64edc7b --- /dev/null +++ b/frontend/src/components/ExternalLink/ExternalLink.tsx @@ -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> = ({ + children, + className, + ...props +}) => ( + + {children} + +); + +export default ExternalLink; diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 7777a40f..fa1efde6 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -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 = ({ session }) => { clientDetails.push({ label: "Uri", value: ( - + {data.ssoLogin?.redirectUri} - + ), }); } diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 30b2ca9b..1e485d94 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -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 = ({ session }) => { }, ]; + const clientTitle = ( + Client + ); const clientDetails = [ { label: "Name", @@ -100,7 +104,7 @@ const OAuth2SessionDetail: React.FC = ({ session }) => {

{deviceId || data.id}

- + {!data.finishedAt && }
diff --git a/frontend/src/components/SessionDetail/SessionDetails.tsx b/frontend/src/components/SessionDetail/SessionDetails.tsx index 74eddaaa..0299d391 100644 --- a/frontend/src/components/SessionDetail/SessionDetails.tsx +++ b/frontend/src/components/SessionDetail/SessionDetails.tsx @@ -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[]; }; diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index a9670f00..75347948 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -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. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 784b36a2..339cdcf5 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -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; +} & { " $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; - 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; +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; 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, diff --git a/frontend/src/pages/OAuth2Client.tsx b/frontend/src/pages/OAuth2Client.tsx index 3af2e258..f0921cf3 100644 --- a/frontend/src/pages/OAuth2Client.tsx +++ b/frontend/src/pages/OAuth2Client.tsx @@ -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 ; const oauth2Client = unwrapOk(result); - if (oauth2Client === null) return ; + if (!oauth2Client) return ; - return ( -
-      {JSON.stringify(oauth2Client, null, 2)}
-    
- ); + return ; }; export default OAuth2Client;