diff --git a/crates/handlers/src/graphql/mutations/mod.rs b/crates/handlers/src/graphql/mutations/mod.rs index dbc56a518..7c8b797bd 100644 --- a/crates/handlers/src/graphql/mutations/mod.rs +++ b/crates/handlers/src/graphql/mutations/mod.rs @@ -8,6 +8,7 @@ mod browser_session; mod compat_session; mod matrix; mod oauth2_session; +mod upstream_oauth; mod user; mod user_email; @@ -22,6 +23,7 @@ pub struct Mutation( compat_session::CompatSessionMutations, browser_session::BrowserSessionMutations, matrix::MatrixMutations, + upstream_oauth::UpstreamOauthMutations, ); impl Mutation { diff --git a/crates/handlers/src/graphql/mutations/upstream_oauth.rs b/crates/handlers/src/graphql/mutations/upstream_oauth.rs new file mode 100644 index 000000000..326c4ad68 --- /dev/null +++ b/crates/handlers/src/graphql/mutations/upstream_oauth.rs @@ -0,0 +1,152 @@ +// Copyright 2024 New Vector Ltd. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use anyhow::Context as _; +use async_graphql::{Context, Description, Enum, InputObject, Object, ID}; +use mas_storage::{user::UserRepository, RepositoryAccess}; + +use crate::graphql::{ + model::{NodeType, UpstreamOAuth2Link, UpstreamOAuth2Provider, User}, + state::ContextExt, +}; + +#[derive(Default)] +pub struct UpstreamOauthMutations { + _private: (), +} + +/// The input for the `removeEmail` mutation +#[derive(InputObject)] +struct RemoveUpstreamLinkInput { + /// The ID of the upstream link to remove + upstream_link_id: ID, +} + +/// The status of the `removeEmail` mutation +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +enum RemoveUpstreamLinkStatus { + /// The upstream link was removed + Removed, + + /// The upstream link was not found + NotFound, +} + +/// The payload of the `removeEmail` mutation +#[derive(Description)] +enum RemoveUpstreamLinkPayload { + Removed(mas_data_model::UpstreamOAuthLink), + NotFound, +} + +#[Object(use_type_description)] +impl RemoveUpstreamLinkPayload { + /// Status of the operation + async fn status(&self) -> RemoveUpstreamLinkStatus { + match self { + RemoveUpstreamLinkPayload::Removed(_) => RemoveUpstreamLinkStatus::Removed, + RemoveUpstreamLinkPayload::NotFound => RemoveUpstreamLinkStatus::NotFound, + } + } + + /// The upstream link that was removed + async fn upstream_link(&self) -> Option { + match self { + RemoveUpstreamLinkPayload::Removed(link) => Some(UpstreamOAuth2Link::new(link.clone())), + RemoveUpstreamLinkPayload::NotFound => None, + } + } + + /// The provider to which the upstream link belonged + async fn provider( + &self, + ctx: &Context<'_>, + ) -> Result, async_graphql::Error> { + let state = ctx.state(); + let provider_id = match self { + RemoveUpstreamLinkPayload::Removed(link) => link.provider_id, + RemoveUpstreamLinkPayload::NotFound => return Ok(None), + }; + + let mut repo = state.repository().await?; + let provider = repo + .upstream_oauth_provider() + .lookup(provider_id) + .await? + .context("Upstream OAuth 2.0 provider not found")?; + + Ok(Some(UpstreamOAuth2Provider::new(provider))) + } + + /// The user to whom the upstream link belonged + async fn user(&self, ctx: &Context<'_>) -> Result, async_graphql::Error> { + let state = ctx.state(); + let mut repo = state.repository().await?; + + let user_id = match self { + RemoveUpstreamLinkPayload::Removed(link) => link.user_id, + RemoveUpstreamLinkPayload::NotFound => return Ok(None), + }; + + match user_id { + None => return Ok(None), + Some(user_id) => { + let user = repo + .user() + .lookup(user_id) + .await? + .context("User not found")?; + + Ok(Some(User(user))) + } + } + } +} + +#[Object] +impl UpstreamOauthMutations { + /// Remove an upstream linked account + async fn remove_upstream_link( + &self, + ctx: &Context<'_>, + input: RemoveUpstreamLinkInput, + ) -> Result { + let state = ctx.state(); + let upstream_link_id = + NodeType::UpstreamOAuth2Link.extract_ulid(&input.upstream_link_id)?; + let requester = ctx.requester(); + + let mut repo = state.repository().await?; + + let upstream_link = repo.upstream_oauth_link().lookup(upstream_link_id).await?; + let Some(upstream_link) = upstream_link else { + return Ok(RemoveUpstreamLinkPayload::NotFound); + }; + + if !requester.is_owner_or_admin(&upstream_link) { + return Ok(RemoveUpstreamLinkPayload::NotFound); + } + + // Allow non-admins to remove their email address if the site config allows it + if !requester.is_admin() && !state.site_config().email_change_allowed { + return Err(async_graphql::Error::new("Unauthorized")); + } + + let upstream_link = repo + .upstream_oauth_link() + .lookup(upstream_link.id) + .await? + .context("Failed to load user")?; + + repo.upstream_oauth_link() + .remove(upstream_link.clone()) + .await?; + + repo.save().await?; + + Ok(RemoveUpstreamLinkPayload::Removed(upstream_link)) + } +} diff --git a/crates/storage-pg/.sqlx/query-5edffe16eb0e038083d423bcd753300d2ba034f0e350ad57ab9aeeddefd88701.json b/crates/storage-pg/.sqlx/query-5edffe16eb0e038083d423bcd753300d2ba034f0e350ad57ab9aeeddefd88701.json new file mode 100644 index 000000000..9cd299b13 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-5edffe16eb0e038083d423bcd753300d2ba034f0e350ad57ab9aeeddefd88701.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE upstream_oauth_authorization_sessions SET upstream_oauth_link_id = NULL\n WHERE upstream_oauth_link_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5edffe16eb0e038083d423bcd753300d2ba034f0e350ad57ab9aeeddefd88701" +} diff --git a/crates/storage-pg/.sqlx/query-cc60ad934d347fb4546205d1fe07e9d2f127cb15b1bb650d1ea3805a4c55b196.json b/crates/storage-pg/.sqlx/query-cc60ad934d347fb4546205d1fe07e9d2f127cb15b1bb650d1ea3805a4c55b196.json new file mode 100644 index 000000000..00e04bffa --- /dev/null +++ b/crates/storage-pg/.sqlx/query-cc60ad934d347fb4546205d1fe07e9d2f127cb15b1bb650d1ea3805a4c55b196.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM upstream_oauth_links\n WHERE upstream_oauth_link_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "cc60ad934d347fb4546205d1fe07e9d2f127cb15b1bb650d1ea3805a4c55b196" +} diff --git a/crates/storage-pg/src/upstream_oauth2/link.rs b/crates/storage-pg/src/upstream_oauth2/link.rs index 02ae4ca62..35a265744 100644 --- a/crates/storage-pg/src/upstream_oauth2/link.rs +++ b/crates/storage-pg/src/upstream_oauth2/link.rs @@ -355,4 +355,46 @@ impl<'c> UpstreamOAuthLinkRepository for PgUpstreamOAuthLinkRepository<'c> { .try_into() .map_err(DatabaseError::to_invalid_operation) } + + #[tracing::instrument( + name = "db.upstream_oauth_link.remove", + skip_all, + fields( + db.query.text, + upstream_oauth_link.id, + upstream_oauth_link.provider_id, + %upstream_oauth_link.subject, + ), + err, + )] + async fn remove(&mut self, upstream_oauth_link: UpstreamOAuthLink) -> Result<(), Self::Error> { + // Unset the authorization sessions first, as they have a foreign key + // constraint on the links. + sqlx::query!( + r#" + UPDATE upstream_oauth_authorization_sessions SET upstream_oauth_link_id = NULL + WHERE upstream_oauth_link_id = $1 + "#, + Uuid::from(upstream_oauth_link.id), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + // Then delete the link itself + let res = sqlx::query!( + r#" + DELETE FROM upstream_oauth_links + WHERE upstream_oauth_link_id = $1 + "#, + Uuid::from(upstream_oauth_link.id), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + Ok(()) + } } diff --git a/crates/storage/src/upstream_oauth2/link.rs b/crates/storage/src/upstream_oauth2/link.rs index 3088fbab0..37056bcb0 100644 --- a/crates/storage/src/upstream_oauth2/link.rs +++ b/crates/storage/src/upstream_oauth2/link.rs @@ -184,6 +184,17 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync { /// /// Returns [`Self::Error`] if the underlying repository fails async fn count(&mut self, filter: UpstreamOAuthLinkFilter<'_>) -> Result; + + /// Delete a [`UpstreamOAuthLink`] + /// + /// # Parameters + /// + /// * `upstream_oauth_link`: The [`UpstreamOAuthLink`] to delete + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn remove(&mut self, upstream_oauth_link: UpstreamOAuthLink) -> Result<(), Self::Error>; } repository_impl!(UpstreamOAuthLinkRepository: @@ -216,4 +227,6 @@ repository_impl!(UpstreamOAuthLinkRepository: ) -> Result, Self::Error>; async fn count(&mut self, filter: UpstreamOAuthLinkFilter<'_>) -> Result; + + async fn remove(&mut self, upstream_oauth_link: UpstreamOAuthLink) -> Result<(), Self::Error>; ); diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 2d91b9052..5927d3929 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -818,6 +818,12 @@ type Mutation { Set the display name of a user """ setDisplayName(input: SetDisplayNameInput!): SetDisplayNamePayload! + """ + Remove an upstream linked account + """ + removeUpstreamLink( + input: RemoveUpstreamLinkInput! + ): RemoveUpstreamLinkPayload! } """ @@ -1161,6 +1167,52 @@ enum RemoveEmailStatus { NOT_FOUND } +""" +The input for the `removeEmail` mutation +""" +input RemoveUpstreamLinkInput { + """ + The ID of the upstream link to remove + """ + upstreamLinkId: ID! +} + +""" +The payload of the `removeEmail` mutation +""" +type RemoveUpstreamLinkPayload { + """ + Status of the operation + """ + status: RemoveUpstreamLinkStatus! + """ + The upstream link that was removed + """ + upstreamLink: UpstreamOAuth2Link + """ + The provider to which the upstream link belonged + """ + provider: UpstreamOAuth2Provider + """ + The user to whom the upstream link belonged + """ + user: User +} + +""" +The status of the `removeEmail` mutation +""" +enum RemoveUpstreamLinkStatus { + """ + The upstream link was removed + """ + REMOVED + """ + The upstream link was not found + """ + NOT_FOUND +} + """ The input for the `sendVerificationEmail` mutation """ diff --git a/frontend/src/components/UpstreamProvider/UnlinkUpstreamProvider.tsx b/frontend/src/components/UpstreamProvider/UnlinkUpstreamProvider.tsx index 1717c9d9b..3de2100f0 100644 --- a/frontend/src/components/UpstreamProvider/UnlinkUpstreamProvider.tsx +++ b/frontend/src/components/UpstreamProvider/UnlinkUpstreamProvider.tsx @@ -8,6 +8,7 @@ import IconSignOut from "@vector-im/compound-design-tokens/assets/web/icons/sign import { Button } from "@vector-im/compound-web"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { useMutation } from "urql"; import { FragmentType, graphql, useFragment } from "../../gql"; import * as Dialog from "../Dialog"; @@ -27,15 +28,25 @@ const FRAGMENT = graphql(/* GraphQL */ ` } `); +export const MUTATION = graphql(/* GraphQL */ ` + mutation RemoveUpstreamLink($id: ID!) { + removeUpstreamLink(input: { upstreamLinkId: $id }) { + status + } + } +`); + const UnlinkUpstreamButton: React.FC< React.PropsWithChildren<{ upstreamProvider: FragmentType; + onUnlinked?: () => void; }> -> = ({ children, upstreamProvider }) => { +> = ({ children, upstreamProvider, onUnlinked }) => { const [inProgress, setInProgress] = useState(false); const [open, setOpen] = useState(false); const { t } = useTranslation(); const data = useFragment(FRAGMENT, upstreamProvider); + const [, removeUpstreamLink] = useMutation(MUTATION); const onConfirm = async ( e: React.MouseEvent, @@ -43,7 +54,16 @@ const UnlinkUpstreamButton: React.FC< e.preventDefault(); setInProgress(true); - // TODO: Unlink + if (!data.upstreamOauth2LinksForUser) { + return; + } + + // We assume only one exists but since its an array we remove all + for (const link of data.upstreamOauth2LinksForUser) { + // FIXME: We should handle errors here + await removeUpstreamLink({ id: link.id }); + } + onUnlinked && onUnlinked(); setInProgress(false); }; diff --git a/frontend/src/components/UserProfile/UpstreamProviderList.tsx b/frontend/src/components/UserProfile/UpstreamProviderList.tsx index 5adc3d9c3..7d991ba3e 100644 --- a/frontend/src/components/UserProfile/UpstreamProviderList.tsx +++ b/frontend/src/components/UserProfile/UpstreamProviderList.tsx @@ -5,7 +5,7 @@ // Please see LICENSE in the repository root for full details. import { H5 } from "@vector-im/compound-web"; -import { useTransition } from "react"; +import { useCallback, useTransition } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "urql"; @@ -58,7 +58,7 @@ const UpstreamProviderList: React.FC<{}> = () => { const { t } = useTranslation(); const [pagination, setPagination] = usePagination(); - const [result] = useQuery({ + const [result, update] = useQuery({ query: QUERY, variables: { ...pagination }, }); @@ -73,6 +73,10 @@ const UpstreamProviderList: React.FC<{}> = () => { }); }; + const onUnlinked = useCallback(() => { + update({ requestPolicy: "network-only" }); + }, [update]); + return ( <>
{t("frontend.account.unlinked_upstreams")}
@@ -99,6 +103,7 @@ const UpstreamProviderList: React.FC<{}> = () => { .map((edge) => ( ))} diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 657b2c917..812990cee 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -30,6 +30,7 @@ const documents = { "\n fragment UnverifiedEmailAlert_user on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n": types.UnverifiedEmailAlert_UserFragmentDoc, "\n fragment LinkUpstreamProvider_provider on UpstreamOAuth2Provider {\n id\n humanName\n upstreamOauth2LinksForUser {\n id\n provider {\n id\n }\n }\n }\n": types.LinkUpstreamProvider_ProviderFragmentDoc, "\n fragment UnlinkUpstreamProvider_provider on UpstreamOAuth2Provider {\n id\n createdAt\n humanName\n upstreamOauth2LinksForUser {\n id\n provider {\n id\n }\n }\n }\n": types.UnlinkUpstreamProvider_ProviderFragmentDoc, + "\n mutation RemoveUpstreamLink($id: ID!) {\n removeUpstreamLink(input: { upstreamLinkId: $id }) {\n status\n }\n }\n": types.RemoveUpstreamLinkDocument, "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n": types.UserEmail_EmailFragmentDoc, "\n fragment UserEmail_siteConfig on SiteConfig {\n id\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc, "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, @@ -145,6 +146,10 @@ export function graphql(source: "\n fragment LinkUpstreamProvider_provider on U * 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 UnlinkUpstreamProvider_provider on UpstreamOAuth2Provider {\n id\n createdAt\n humanName\n upstreamOauth2LinksForUser {\n id\n provider {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment UnlinkUpstreamProvider_provider on UpstreamOAuth2Provider {\n id\n createdAt\n humanName\n upstreamOauth2LinksForUser {\n id\n provider {\n id\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 mutation RemoveUpstreamLink($id: ID!) {\n removeUpstreamLink(input: { upstreamLinkId: $id }) {\n status\n }\n }\n"): (typeof documents)["\n mutation RemoveUpstreamLink($id: ID!) {\n removeUpstreamLink(input: { upstreamLinkId: $id }) {\n status\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 76f2bd4d9..b45c967a2 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -503,6 +503,8 @@ export type Mutation = { lockUser: LockUserPayload; /** Remove an email address */ removeEmail: RemoveEmailPayload; + /** Remove an upstream linked account */ + removeUpstreamLink: RemoveUpstreamLinkPayload; /** Send a verification code for an email address */ sendVerificationEmail: SendVerificationEmailPayload; /** @@ -586,6 +588,12 @@ export type MutationRemoveEmailArgs = { }; +/** The mutations root of the GraphQL interface. */ +export type MutationRemoveUpstreamLinkArgs = { + input: RemoveUpstreamLinkInput; +}; + + /** The mutations root of the GraphQL interface. */ export type MutationSendVerificationEmailArgs = { input: SendVerificationEmailInput; @@ -899,6 +907,33 @@ export enum RemoveEmailStatus { Removed = 'REMOVED' } +/** The input for the `removeEmail` mutation */ +export type RemoveUpstreamLinkInput = { + /** The ID of the upstream link to remove */ + upstreamLinkId: Scalars['ID']['input']; +}; + +/** The payload of the `removeEmail` mutation */ +export type RemoveUpstreamLinkPayload = { + __typename?: 'RemoveUpstreamLinkPayload'; + /** The provider to which the upstream link belonged */ + provider?: Maybe; + /** Status of the operation */ + status: RemoveUpstreamLinkStatus; + /** The upstream link that was removed */ + upstreamLink?: Maybe; + /** The user to whom the upstream link belonged */ + user?: Maybe; +}; + +/** The status of the `removeEmail` mutation */ +export enum RemoveUpstreamLinkStatus { + /** The upstream link was not found */ + NotFound = 'NOT_FOUND', + /** The upstream link was removed */ + Removed = 'REMOVED' +} + /** The input for the `sendVerificationEmail` mutation */ export type SendVerificationEmailInput = { /** The ID of the email address to verify */ @@ -1503,6 +1538,13 @@ export type LinkUpstreamProvider_ProviderFragment = { __typename?: 'UpstreamOAut export type UnlinkUpstreamProvider_ProviderFragment = { __typename?: 'UpstreamOAuth2Provider', id: string, createdAt: string, humanName?: string | null, upstreamOauth2LinksForUser: Array<{ __typename?: 'UpstreamOAuth2Link', id: string, provider: { __typename?: 'UpstreamOAuth2Provider', id: string } }> } & { ' $fragmentName'?: 'UnlinkUpstreamProvider_ProviderFragment' }; +export type RemoveUpstreamLinkMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type RemoveUpstreamLinkMutation = { __typename?: 'Mutation', removeUpstreamLink: { __typename?: 'RemoveUpstreamLinkPayload', status: RemoveUpstreamLinkStatus } }; + export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string, confirmedAt?: string | null } & { ' $fragmentName'?: 'UserEmail_EmailFragment' }; export type UserEmail_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmail_SiteConfigFragment' }; @@ -1781,6 +1823,7 @@ export const EndBrowserSessionDocument = {"kind":"Document","definitions":[{"kin export const EndCompatSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndCompatSession"},"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":"endCompatSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"compatSessionId"},"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":"compatSession"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const FooterQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FooterQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"Footer_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Footer_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imprint"}},{"kind":"Field","name":{"kind":"Name","value":"tosUri"}},{"kind":"Field","name":{"kind":"Name","value":"policyUri"}}]}}]} as unknown as DocumentNode; export const EndOAuth2SessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndOAuth2Session"},"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":"endOauth2Session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"oauth2SessionId"},"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":"oauth2Session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"OAuth2Session_session"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OAuth2Session_session"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Oauth2Session"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"scope"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"deviceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"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":"applicationType"}},{"kind":"Field","name":{"kind":"Name","value":"logoUri"}}]}}]}}]} as unknown as DocumentNode; +export const RemoveUpstreamLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveUpstreamLink"},"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":"removeUpstreamLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"upstreamLinkId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode; export const RemoveEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveEmail"},"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":"removeEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userEmailId"},"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":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const SetPrimaryEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetPrimaryEmail"},"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":"setPrimaryEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userEmailId"},"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":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"primaryEmail"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const SetDisplayNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetDisplayName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"displayName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setDisplayName"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"displayName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"displayName"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"matrix"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/frontend/src/gql/schema.ts b/frontend/src/gql/schema.ts index caa523c98..f9e9d0bb1 100644 --- a/frontend/src/gql/schema.ts +++ b/frontend/src/gql/schema.ts @@ -1404,6 +1404,29 @@ export default { } ] }, + { + "name": "removeUpstreamLink", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "OBJECT", + "name": "RemoveUpstreamLinkPayload", + "ofType": null + } + }, + "args": [ + { + "name": "input", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Any" + } + } + } + ] + }, { "name": "sendVerificationEmail", "type": { @@ -2445,6 +2468,51 @@ export default { ], "interfaces": [] }, + { + "kind": "OBJECT", + "name": "RemoveUpstreamLinkPayload", + "fields": [ + { + "name": "provider", + "type": { + "kind": "OBJECT", + "name": "UpstreamOAuth2Provider", + "ofType": null + }, + "args": [] + }, + { + "name": "status", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Any" + } + }, + "args": [] + }, + { + "name": "upstreamLink", + "type": { + "kind": "OBJECT", + "name": "UpstreamOAuth2Link", + "ofType": null + }, + "args": [] + }, + { + "name": "user", + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "args": [] + } + ], + "interfaces": [] + }, { "kind": "OBJECT", "name": "SendVerificationEmailPayload",