1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Ability to remove emails

This commit is contained in:
Quentin Gliech
2023-04-28 17:11:23 +02:00
parent e8b7591d7e
commit 741873b84e
14 changed files with 2085 additions and 1629 deletions

1
Cargo.lock generated
View File

@ -3224,6 +3224,7 @@ dependencies = [
"async-graphql",
"async-trait",
"chrono",
"lettre",
"mas-data-model",
"mas-storage",
"oauth2-types",

View File

@ -10,6 +10,7 @@ anyhow = "1.0.71"
async-graphql = { version = "5.0.9", features = ["chrono", "url"] }
async-trait = "0.1.68"
chrono = "0.4.24"
lettre = { version = "0.10.4", default-features = false }
serde = { version = "1.0.163", features = ["derive"] }
thiserror = "1.0.40"
tokio = { version = "1.28.1", features = ["sync"] }

View File

@ -46,6 +46,8 @@ pub enum AddEmailStatus {
Added,
/// The email address already exists
Exists,
/// The email address is invalid
Invalid,
}
/// The payload of the `addEmail` mutation
@ -53,6 +55,7 @@ pub enum AddEmailStatus {
enum AddEmailPayload {
Added(mas_data_model::UserEmail),
Exists(mas_data_model::UserEmail),
Invalid,
}
#[Object(use_type_description)]
@ -62,25 +65,28 @@ impl AddEmailPayload {
match self {
AddEmailPayload::Added(_) => AddEmailStatus::Added,
AddEmailPayload::Exists(_) => AddEmailStatus::Exists,
AddEmailPayload::Invalid => AddEmailStatus::Invalid,
}
}
/// The email address that was added
async fn email(&self) -> UserEmail {
async fn email(&self) -> Option<UserEmail> {
match self {
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => {
UserEmail(email.clone())
Some(UserEmail(email.clone()))
}
AddEmailPayload::Invalid => None,
}
}
/// The user to whom the email address was added
async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_id = match self {
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => email.user_id,
AddEmailPayload::Invalid => return Ok(None),
};
let user = repo
@ -89,7 +95,7 @@ impl AddEmailPayload {
.await?
.context("User not found")?;
Ok(User(user))
Ok(Some(User(user)))
}
}
@ -227,6 +233,77 @@ impl VerifyEmailPayload {
}
}
/// The input for the `removeEmail` mutation
#[derive(InputObject)]
struct RemoveEmailInput {
/// The ID of the email address to remove
user_email_id: ID,
}
/// The status of the `removeEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum RemoveEmailStatus {
/// The email address was removed
Removed,
/// Can't remove the primary email address
Primary,
/// The email address was not found
NotFound,
}
/// The payload of the `removeEmail` mutation
#[derive(Description)]
enum RemoveEmailPayload {
Removed(mas_data_model::UserEmail),
Primary(mas_data_model::UserEmail),
NotFound,
}
#[Object(use_type_description)]
impl RemoveEmailPayload {
/// Status of the operation
async fn status(&self) -> RemoveEmailStatus {
match self {
RemoveEmailPayload::Removed(_) => RemoveEmailStatus::Removed,
RemoveEmailPayload::Primary(_) => RemoveEmailStatus::Primary,
RemoveEmailPayload::NotFound => RemoveEmailStatus::NotFound,
}
}
/// The email address that was removed
async fn email(&self) -> Option<UserEmail> {
match self {
RemoveEmailPayload::Removed(email) | RemoveEmailPayload::Primary(email) => {
Some(UserEmail(email.clone()))
}
RemoveEmailPayload::NotFound => None,
}
}
/// The user to whom the email address belonged
async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_id = match self {
RemoveEmailPayload::Removed(email) | RemoveEmailPayload::Primary(email) => {
email.user_id
}
RemoveEmailPayload::NotFound => return Ok(None),
};
let user = repo
.user()
.lookup(user_id)
.await?
.context("User not found")?;
Ok(Some(User(user)))
}
}
#[Object]
impl UserEmailMutations {
/// Add an email address to the specified user
@ -249,6 +326,12 @@ impl UserEmailMutations {
// XXX: this logic should be extracted somewhere else, since most of it is
// duplicated in mas_handlers
// Validate the email address
if input.email.parse::<lettre::Address>().is_err() {
return Ok(AddEmailPayload::Invalid);
}
// Find an existing email address
let existing_user_email = repo.user_email().find(user, &input.email).await?;
let (added, user_email) = if let Some(user_email) = existing_user_email {
@ -388,4 +471,39 @@ impl UserEmailMutations {
Ok(VerifyEmailPayload::Verified(user_email))
}
/// Remove an email address
async fn remove_email(
&self,
ctx: &Context<'_>,
input: RemoveEmailInput,
) -> Result<RemoveEmailPayload, async_graphql::Error> {
let state = ctx.state();
let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
let requester = ctx.requester();
let user = requester.user().context("Unauthorized")?;
let mut repo = state.repository().await?;
let user_email = repo.user_email().lookup(user_email_id).await?;
let Some(user_email) = user_email else {
return Ok(RemoveEmailPayload::NotFound);
};
if user_email.user_id != user.id {
return Err(async_graphql::Error::new("Unauthorized"));
}
if user.primary_user_email_id == Some(user_email.id) {
// Prevent removing the primary email address
return Ok(RemoveEmailPayload::Primary(user_email));
}
repo.user_email().remove(user_email.clone()).await?;
repo.save().await?;
Ok(RemoveEmailPayload::Removed(user_email))
}
}

View File

@ -23,11 +23,11 @@ type AddEmailPayload {
"""
The email address that was added
"""
email: UserEmail!
email: UserEmail
"""
The user to whom the email address was added
"""
user: User!
user: User
}
"""
@ -42,6 +42,10 @@ enum AddEmailStatus {
The email address already exists
"""
EXISTS
"""
The email address is invalid
"""
INVALID
}
type Anonymous implements Node {
@ -237,6 +241,10 @@ type Mutation {
Submit a verification code for an email address
"""
verifyEmail(input: VerifyEmailInput!): VerifyEmailPayload!
"""
Remove an email address
"""
removeEmail(input: RemoveEmailInput!): RemoveEmailPayload!
}
"""
@ -421,6 +429,52 @@ type Query {
viewerSession: ViewerSession!
}
"""
The input for the `removeEmail` mutation
"""
input RemoveEmailInput {
"""
The ID of the email address to remove
"""
userEmailId: ID!
}
"""
The payload of the `removeEmail` mutation
"""
type RemoveEmailPayload {
"""
Status of the operation
"""
status: RemoveEmailStatus!
"""
The email address that was removed
"""
email: UserEmail
"""
The user to whom the email address belonged
"""
user: User
}
"""
The status of the `removeEmail` mutation
"""
enum RemoveEmailStatus {
"""
The email address was removed
"""
REMOVED
"""
Can't remove the primary email address
"""
PRIMARY
"""
The email address was not found
"""
NOT_FOUND
}
"""
The input for the `sendVerificationEmail` mutation
"""

View File

@ -58,7 +58,12 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
startTransition(() => {
addEmail({ userId, email }).then(() => {
addEmail({ userId, email }).then((result) => {
// Don't clear the form if the email was invalid
if (result.data?.addEmail.status === "INVALID") {
return;
}
startTransition(() => {
// Paginate to the last page
setCurrentPagination(LAST_PAGE);
@ -74,22 +79,33 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
});
};
const status = addEmailResult.data?.addEmail.status ?? null;
const emailAdded = status === "ADDED";
const emailExists = status === "EXISTS";
const emailInvalid = status === "INVALID";
return (
<>
{addEmailResult.data?.addEmail.status === "ADDED" && (
<>
{emailAdded && (
<div className="pt-4">
<Typography variant="subtitle">Email added!</Typography>
</div>
</>
)}
{addEmailResult.data?.addEmail.status === "EXISTS" && (
<>
{emailExists && (
<div className="pt-4">
<Typography variant="subtitle">Email already exists!</Typography>
</div>
</>
)}
{emailInvalid && (
<div className="pt-4 text-alert">
<Typography variant="subtitle" bold>
Invalid email address
</Typography>
</div>
)}
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
<Input
className="flex-1 mr-2"

View File

@ -13,14 +13,18 @@
// limitations under the License.
import { atom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
import { currentBrowserSessionIdAtom } from "../atoms";
import { graphql } from "../gql";
import { PageInfo } from "../gql/graphql";
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
import {
atomForCurrentPagination,
atomWithPagination,
Pagination,
} from "../pagination";
import BlockList from "./BlockList";
import BrowserSession from "./BrowserSession";
@ -62,15 +66,12 @@ const QUERY = graphql(/* GraphQL */ `
}
`);
const currentPagination = atomWithDefault<Pagination>((get) => ({
first: get(pageSizeAtom),
after: null,
}));
const currentPaginationAtom = atomForCurrentPagination();
const browserSessionListFamily = atomFamily((userId: string) => {
const browserSessionList = atomWithQuery({
query: QUERY,
getVariables: (get) => ({ userId, ...get(currentPagination) }),
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
});
return browserSessionList;
});
@ -85,7 +86,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
const paginationFamily = atomFamily((userId: string) => {
const paginationAtom = atomWithPagination(
currentPagination,
currentPaginationAtom,
pageInfoFamily(userId)
);
@ -96,7 +97,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
const currentSessionId = useAtomValue(currentBrowserSessionIdAtom);
const [pending, startTransition] = useTransition();
const result = useAtomValue(browserSessionListFamily(userId));
const setPagination = useSetAtom(currentPagination);
const setPagination = useSetAtom(currentPaginationAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
const paginate = (pagination: Pagination) => {

View File

@ -13,13 +13,17 @@
// limitations under the License.
import { atom, useSetAtom, useAtomValue } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
import { graphql } from "../gql";
import { PageInfo } from "../gql/graphql";
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
import {
atomForCurrentPagination,
atomWithPagination,
Pagination,
} from "../pagination";
import BlockList from "./BlockList";
import CompatSsoLogin from "./CompatSsoLogin";
@ -60,15 +64,12 @@ const QUERY = graphql(/* GraphQL */ `
}
`);
const currentPagination = atomWithDefault<Pagination>((get) => ({
first: get(pageSizeAtom),
after: null,
}));
const currentPaginationAtom = atomForCurrentPagination();
const compatSsoLoginListFamily = atomFamily((userId: string) => {
const compatSsoLoginList = atomWithQuery({
query: QUERY,
getVariables: (get) => ({ userId, ...get(currentPagination) }),
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
});
return compatSsoLoginList;
@ -85,7 +86,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
const paginationFamily = atomFamily((userId: string) => {
const paginationAtom = atomWithPagination(
currentPagination,
currentPaginationAtom,
pageInfoFamily(userId)
);
return paginationAtom;
@ -94,7 +95,7 @@ const paginationFamily = atomFamily((userId: string) => {
const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
const [pending, startTransition] = useTransition();
const result = useAtomValue(compatSsoLoginListFamily(userId));
const setPagination = useSetAtom(currentPagination);
const setPagination = useSetAtom(currentPaginationAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
const paginate = (pagination: Pagination) => {

View File

@ -13,13 +13,17 @@
// limitations under the License.
import { useAtomValue, atom, useSetAtom } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
import { graphql } from "../gql";
import { PageInfo } from "../gql/graphql";
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
import {
atomForCurrentPagination,
atomWithPagination,
Pagination,
} from "../pagination";
import BlockList from "./BlockList";
import OAuth2Session from "./OAuth2Session";
@ -61,15 +65,12 @@ const QUERY = graphql(/* GraphQL */ `
}
`);
const currentPagination = atomWithDefault<Pagination>((get) => ({
first: get(pageSizeAtom),
after: null,
}));
const currentPaginationAtom = atomForCurrentPagination();
const oauth2SessionListFamily = atomFamily((userId: string) => {
const oauth2SessionList = atomWithQuery({
query: QUERY,
getVariables: (get) => ({ userId, ...get(currentPagination) }),
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
});
return oauth2SessionList;
@ -86,7 +87,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
const paginationFamily = atomFamily((userId: string) => {
const paginationAtom = atomWithPagination(
currentPagination,
currentPaginationAtom,
pageInfoFamily(userId)
);
return paginationAtom;
@ -99,7 +100,7 @@ type Props = {
const OAuth2SessionList: React.FC<Props> = ({ userId }) => {
const [pending, startTransition] = useTransition();
const result = useAtomValue(oauth2SessionListFamily(userId));
const setPagination = useSetAtom(currentPagination);
const setPagination = useSetAtom(currentPaginationAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
const paginate = (pagination: Pagination) => {

View File

@ -24,6 +24,7 @@ import Button from "./Button";
import DateTime from "./DateTime";
import Input from "./Input";
import Typography, { Bold } from "./Typography";
import { emailPageResultFamily } from "./UserEmailList";
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_email on UserEmail {
@ -74,6 +75,18 @@ const RESEND_VERIFICATION_EMAIL_MUTATION = graphql(/* GraphQL */ `
}
`);
const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation RemoveEmail($id: ID!) {
removeEmail(input: { userEmailId: $id }) {
status
user {
id
}
}
}
`);
const verifyEmailFamily = atomFamily((id: string) => {
const verifyEmail = atomWithMutation(VERIFY_EMAIL_MUTATION);
@ -100,19 +113,35 @@ const resendVerificationEmailFamily = atomFamily((id: string) => {
return resendVerificationEmailAtom;
});
const removeEmailFamily = atomFamily((id: string) => {
const removeEmail = atomWithMutation(REMOVE_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const removeEmailAtom = atom(
(get) => get(removeEmail),
(get, set) => set(removeEmail, { id })
);
return removeEmailAtom;
});
const UserEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
userId: string; // XXX: Can we get this from the fragment?
isPrimary?: boolean;
highlight?: boolean;
}> = ({ email, isPrimary, highlight }) => {
}> = ({ email, userId, isPrimary, highlight }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(FRAGMENT, email);
const [verifyEmailResult, verifyEmail] = useAtom(verifyEmailFamily(data.id));
const [resendVerificationEmailResult, resendVerificationEmail] = useAtom(
resendVerificationEmailFamily(data.id)
);
const resetEmailPage = useSetAtom(emailPageResultFamily(userId));
const removeEmail = useSetAtom(removeEmailFamily(data.id));
const formRef = useRef<HTMLFormElement>(null);
// XXX: we probably want those callbacks passed in props instead
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
@ -132,8 +161,20 @@ const UserEmail: React.FC<{
});
};
const onRemoveClick = () => {
startTransition(() => {
removeEmail().then(() => {
// XXX: this forces a re-fetch of the user's emails,
// but maybe it's not the right place to do this
resetEmailPage();
});
});
};
const emailSent =
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
const invalidCode =
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
return (
<Block highlight={highlight}>
@ -142,9 +183,16 @@ const UserEmail: React.FC<{
Primary
</Typography>
)}
<div className="flex justify-between items-center">
<Typography variant="caption">
<Bold>{data.email}</Bold>
</Typography>
{!isPrimary && (
<Button disabled={pending} onClick={onRemoveClick} compact>
Remove
</Button>
)}
</div>
{data.confirmedAt ? (
<Typography variant="micro">
Verified <DateTime datetime={data.confirmedAt} />
@ -162,6 +210,9 @@ const UserEmail: React.FC<{
type="text"
inputMode="numeric"
/>
{invalidCode && (
<div className="col-span-2 text-alert font-bold">Invalid code</div>
)}
<Button type="submit" disabled={pending}>
Submit
</Button>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { atom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
@ -22,7 +22,6 @@ import { PageInfo } from "../gql/graphql";
import {
atomForCurrentPagination,
atomWithPagination,
pageSizeAtom,
Pagination,
} from "../pagination";
@ -143,6 +142,7 @@ const UserEmailList: React.FC<{
{result.data?.user?.emails?.edges?.map((edge) => (
<UserEmail
email={edge.node}
userId={userId}
key={edge.cursor}
isPrimary={primaryEmailId === edge.node.id}
highlight={highlightedEmail === edge.node.id}

View File

@ -37,6 +37,8 @@ const documents = {
types.VerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.ResendVerificationEmailDocument,
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n":
types.RemoveEmailDocument,
"\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.UserEmailListQueryDocument,
"\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n":
@ -135,6 +137,12 @@ export function graphql(
export function graphql(
source: "\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"
): (typeof documents)["\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\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 RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n"
): (typeof documents)["\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\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.
*/

View File

@ -40,11 +40,11 @@ export type AddEmailInput = {
export type AddEmailPayload = {
__typename?: "AddEmailPayload";
/** The email address that was added */
email: UserEmail;
email?: Maybe<UserEmail>;
/** Status of the operation */
status: AddEmailStatus;
/** The user to whom the email address was added */
user: User;
user?: Maybe<User>;
};
/** The status of the `addEmail` mutation */
@ -53,6 +53,8 @@ export enum AddEmailStatus {
Added = "ADDED",
/** The email address already exists */
Exists = "EXISTS",
/** The email address is invalid */
Invalid = "INVALID",
}
export type Anonymous = Node & {
@ -178,6 +180,8 @@ export type Mutation = {
__typename?: "Mutation";
/** Add an email address to the specified user */
addEmail: AddEmailPayload;
/** Remove an email address */
removeEmail: RemoveEmailPayload;
/** Send a verification code for an email address */
sendVerificationEmail: SendVerificationEmailPayload;
/** Submit a verification code for an email address */
@ -189,6 +193,11 @@ export type MutationAddEmailArgs = {
input: AddEmailInput;
};
/** The mutations root of the GraphQL interface. */
export type MutationRemoveEmailArgs = {
input: RemoveEmailInput;
};
/** The mutations root of the GraphQL interface. */
export type MutationSendVerificationEmailArgs = {
input: SendVerificationEmailInput;
@ -352,6 +361,33 @@ export type QueryUserEmailArgs = {
id: Scalars["ID"];
};
/** The input for the `removeEmail` mutation */
export type RemoveEmailInput = {
/** The ID of the email address to remove */
userEmailId: Scalars["ID"];
};
/** The payload of the `removeEmail` mutation */
export type RemoveEmailPayload = {
__typename?: "RemoveEmailPayload";
/** The email address that was removed */
email?: Maybe<UserEmail>;
/** Status of the operation */
status: RemoveEmailStatus;
/** The user to whom the email address belonged */
user?: Maybe<User>;
};
/** The status of the `removeEmail` mutation */
export enum RemoveEmailStatus {
/** The email address was not found */
NotFound = "NOT_FOUND",
/** Can't remove the primary email address */
Primary = "PRIMARY",
/** The email address was removed */
Removed = "REMOVED",
}
/** The input for the `sendVerificationEmail` mutation */
export type SendVerificationEmailInput = {
/** The ID of the email address to verify */
@ -607,9 +643,13 @@ export type AddEmailMutation = {
addEmail: {
__typename?: "AddEmailPayload";
status: AddEmailStatus;
email: { __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
email?:
| ({ __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: {
UserEmail_EmailFragment: UserEmail_EmailFragment;
};
})
| null;
};
};
@ -808,6 +848,19 @@ export type ResendVerificationEmailMutation = {
};
};
export type RemoveEmailMutationVariables = Exact<{
id: Scalars["ID"];
}>;
export type RemoveEmailMutation = {
__typename?: "Mutation";
removeEmail: {
__typename?: "RemoveEmailPayload";
status: RemoveEmailStatus;
user?: { __typename?: "User"; id: string } | null;
};
};
export type UserEmailListQueryQueryVariables = Exact<{
userId: Scalars["ID"];
first?: InputMaybe<Scalars["Int"]>;
@ -2101,6 +2154,70 @@ export const ResendVerificationEmailDocument = {
ResendVerificationEmailMutation,
ResendVerificationEmailMutationVariables
>;
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<RemoveEmailMutation, RemoveEmailMutationVariables>;
export const UserEmailListQueryDocument = {
kind: "Document",
definitions: [

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,11 @@ import { cacheExchange } from "@urql/exchange-graphcache";
import { refocusExchange } from "@urql/exchange-refocus";
import { requestPolicyExchange } from "@urql/exchange-request-policy";
import type { MutationAddEmailArgs } from "./gql/graphql";
import type {
MutationAddEmailArgs,
MutationRemoveEmailArgs,
RemoveEmailPayload,
} from "./gql/graphql";
import schema from "./gql/schema";
const cache = cacheExchange({
@ -39,6 +43,36 @@ const cache = cacheExchange({
cache.invalidate(key, field.fieldName, field.arguments);
});
},
removeEmail: (
result: { removeEmail?: RemoveEmailPayload },
args: MutationRemoveEmailArgs,
cache,
_info
) => {
// Invalidate the email entity
cache.invalidate({
__typename: "UserEmail",
id: args.input.userEmailId,
});
// Let's try to figure out the userId to invalidate the emails field on the User object
const userId = result.removeEmail?.user?.id;
if (userId) {
const key = cache.keyOfEntity({
__typename: "User",
id: userId,
});
// Invalidate the emails field on the User object so that it gets refetched
cache
.inspectFields(key)
.filter((field) => field.fieldName === "emails")
.forEach((field) => {
cache.invalidate(key, field.fieldName, field.arguments);
});
}
},
},
},
});