You've already forked authentication-service
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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3224,6 +3224,7 @@ dependencies = [
|
|||||||
"async-graphql",
|
"async-graphql",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"lettre",
|
||||||
"mas-data-model",
|
"mas-data-model",
|
||||||
"mas-storage",
|
"mas-storage",
|
||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
|
@ -10,6 +10,7 @@ anyhow = "1.0.71"
|
|||||||
async-graphql = { version = "5.0.9", features = ["chrono", "url"] }
|
async-graphql = { version = "5.0.9", features = ["chrono", "url"] }
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.68"
|
||||||
chrono = "0.4.24"
|
chrono = "0.4.24"
|
||||||
|
lettre = { version = "0.10.4", default-features = false }
|
||||||
serde = { version = "1.0.163", features = ["derive"] }
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
tokio = { version = "1.28.1", features = ["sync"] }
|
tokio = { version = "1.28.1", features = ["sync"] }
|
||||||
|
@ -46,6 +46,8 @@ pub enum AddEmailStatus {
|
|||||||
Added,
|
Added,
|
||||||
/// The email address already exists
|
/// The email address already exists
|
||||||
Exists,
|
Exists,
|
||||||
|
/// The email address is invalid
|
||||||
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The payload of the `addEmail` mutation
|
/// The payload of the `addEmail` mutation
|
||||||
@ -53,6 +55,7 @@ pub enum AddEmailStatus {
|
|||||||
enum AddEmailPayload {
|
enum AddEmailPayload {
|
||||||
Added(mas_data_model::UserEmail),
|
Added(mas_data_model::UserEmail),
|
||||||
Exists(mas_data_model::UserEmail),
|
Exists(mas_data_model::UserEmail),
|
||||||
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Object(use_type_description)]
|
#[Object(use_type_description)]
|
||||||
@ -62,25 +65,28 @@ impl AddEmailPayload {
|
|||||||
match self {
|
match self {
|
||||||
AddEmailPayload::Added(_) => AddEmailStatus::Added,
|
AddEmailPayload::Added(_) => AddEmailStatus::Added,
|
||||||
AddEmailPayload::Exists(_) => AddEmailStatus::Exists,
|
AddEmailPayload::Exists(_) => AddEmailStatus::Exists,
|
||||||
|
AddEmailPayload::Invalid => AddEmailStatus::Invalid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The email address that was added
|
/// The email address that was added
|
||||||
async fn email(&self) -> UserEmail {
|
async fn email(&self) -> Option<UserEmail> {
|
||||||
match self {
|
match self {
|
||||||
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => {
|
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
|
/// 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 state = ctx.state();
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
|
|
||||||
let user_id = match self {
|
let user_id = match self {
|
||||||
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => email.user_id,
|
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => email.user_id,
|
||||||
|
AddEmailPayload::Invalid => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = repo
|
let user = repo
|
||||||
@ -89,7 +95,7 @@ impl AddEmailPayload {
|
|||||||
.await?
|
.await?
|
||||||
.context("User not found")?;
|
.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]
|
#[Object]
|
||||||
impl UserEmailMutations {
|
impl UserEmailMutations {
|
||||||
/// Add an email address to the specified user
|
/// 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
|
// XXX: this logic should be extracted somewhere else, since most of it is
|
||||||
// duplicated in mas_handlers
|
// 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
|
// Find an existing email address
|
||||||
let existing_user_email = repo.user_email().find(user, &input.email).await?;
|
let existing_user_email = repo.user_email().find(user, &input.email).await?;
|
||||||
let (added, user_email) = if let Some(user_email) = existing_user_email {
|
let (added, user_email) = if let Some(user_email) = existing_user_email {
|
||||||
@ -388,4 +471,39 @@ impl UserEmailMutations {
|
|||||||
|
|
||||||
Ok(VerifyEmailPayload::Verified(user_email))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,11 @@ type AddEmailPayload {
|
|||||||
"""
|
"""
|
||||||
The email address that was added
|
The email address that was added
|
||||||
"""
|
"""
|
||||||
email: UserEmail!
|
email: UserEmail
|
||||||
"""
|
"""
|
||||||
The user to whom the email address was added
|
The user to whom the email address was added
|
||||||
"""
|
"""
|
||||||
user: User!
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -42,6 +42,10 @@ enum AddEmailStatus {
|
|||||||
The email address already exists
|
The email address already exists
|
||||||
"""
|
"""
|
||||||
EXISTS
|
EXISTS
|
||||||
|
"""
|
||||||
|
The email address is invalid
|
||||||
|
"""
|
||||||
|
INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
type Anonymous implements Node {
|
type Anonymous implements Node {
|
||||||
@ -237,6 +241,10 @@ type Mutation {
|
|||||||
Submit a verification code for an email address
|
Submit a verification code for an email address
|
||||||
"""
|
"""
|
||||||
verifyEmail(input: VerifyEmailInput!): VerifyEmailPayload!
|
verifyEmail(input: VerifyEmailInput!): VerifyEmailPayload!
|
||||||
|
"""
|
||||||
|
Remove an email address
|
||||||
|
"""
|
||||||
|
removeEmail(input: RemoveEmailInput!): RemoveEmailPayload!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -421,6 +429,52 @@ type Query {
|
|||||||
viewerSession: ViewerSession!
|
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
|
The input for the `sendVerificationEmail` mutation
|
||||||
"""
|
"""
|
||||||
|
@ -58,7 +58,12 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
|
|||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const email = formData.get("email") as string;
|
const email = formData.get("email") as string;
|
||||||
startTransition(() => {
|
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(() => {
|
startTransition(() => {
|
||||||
// Paginate to the last page
|
// Paginate to the last page
|
||||||
setCurrentPagination(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{addEmailResult.data?.addEmail.status === "ADDED" && (
|
{emailAdded && (
|
||||||
<>
|
<div className="pt-4">
|
||||||
<div className="pt-4">
|
<Typography variant="subtitle">Email added!</Typography>
|
||||||
<Typography variant="subtitle">Email added!</Typography>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{addEmailResult.data?.addEmail.status === "EXISTS" && (
|
|
||||||
<>
|
{emailExists && (
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<Typography variant="subtitle">Email already exists!</Typography>
|
<Typography variant="subtitle">Email already exists!</Typography>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{emailInvalid && (
|
||||||
|
<div className="pt-4 text-alert">
|
||||||
|
<Typography variant="subtitle" bold>
|
||||||
|
Invalid email address
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
|
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
|
||||||
<Input
|
<Input
|
||||||
className="flex-1 mr-2"
|
className="flex-1 mr-2"
|
||||||
|
@ -13,14 +13,18 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { atomFamily, atomWithDefault } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { atomWithQuery } from "jotai-urql";
|
import { atomWithQuery } from "jotai-urql";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
import { currentBrowserSessionIdAtom } from "../atoms";
|
import { currentBrowserSessionIdAtom } from "../atoms";
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import { PageInfo } from "../gql/graphql";
|
import { PageInfo } from "../gql/graphql";
|
||||||
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
|
import {
|
||||||
|
atomForCurrentPagination,
|
||||||
|
atomWithPagination,
|
||||||
|
Pagination,
|
||||||
|
} from "../pagination";
|
||||||
|
|
||||||
import BlockList from "./BlockList";
|
import BlockList from "./BlockList";
|
||||||
import BrowserSession from "./BrowserSession";
|
import BrowserSession from "./BrowserSession";
|
||||||
@ -62,15 +66,12 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const currentPagination = atomWithDefault<Pagination>((get) => ({
|
const currentPaginationAtom = atomForCurrentPagination();
|
||||||
first: get(pageSizeAtom),
|
|
||||||
after: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const browserSessionListFamily = atomFamily((userId: string) => {
|
const browserSessionListFamily = atomFamily((userId: string) => {
|
||||||
const browserSessionList = atomWithQuery({
|
const browserSessionList = atomWithQuery({
|
||||||
query: QUERY,
|
query: QUERY,
|
||||||
getVariables: (get) => ({ userId, ...get(currentPagination) }),
|
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||||
});
|
});
|
||||||
return browserSessionList;
|
return browserSessionList;
|
||||||
});
|
});
|
||||||
@ -85,7 +86,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
|
|||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
const paginationFamily = atomFamily((userId: string) => {
|
||||||
const paginationAtom = atomWithPagination(
|
const paginationAtom = atomWithPagination(
|
||||||
currentPagination,
|
currentPaginationAtom,
|
||||||
pageInfoFamily(userId)
|
pageInfoFamily(userId)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
|||||||
const currentSessionId = useAtomValue(currentBrowserSessionIdAtom);
|
const currentSessionId = useAtomValue(currentBrowserSessionIdAtom);
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(browserSessionListFamily(userId));
|
const result = useAtomValue(browserSessionListFamily(userId));
|
||||||
const setPagination = useSetAtom(currentPagination);
|
const setPagination = useSetAtom(currentPaginationAtom);
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||||
|
|
||||||
const paginate = (pagination: Pagination) => {
|
const paginate = (pagination: Pagination) => {
|
||||||
|
@ -13,13 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { atom, useSetAtom, useAtomValue } from "jotai";
|
import { atom, useSetAtom, useAtomValue } from "jotai";
|
||||||
import { atomFamily, atomWithDefault } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { atomWithQuery } from "jotai-urql";
|
import { atomWithQuery } from "jotai-urql";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import { PageInfo } from "../gql/graphql";
|
import { PageInfo } from "../gql/graphql";
|
||||||
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
|
import {
|
||||||
|
atomForCurrentPagination,
|
||||||
|
atomWithPagination,
|
||||||
|
Pagination,
|
||||||
|
} from "../pagination";
|
||||||
|
|
||||||
import BlockList from "./BlockList";
|
import BlockList from "./BlockList";
|
||||||
import CompatSsoLogin from "./CompatSsoLogin";
|
import CompatSsoLogin from "./CompatSsoLogin";
|
||||||
@ -60,15 +64,12 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const currentPagination = atomWithDefault<Pagination>((get) => ({
|
const currentPaginationAtom = atomForCurrentPagination();
|
||||||
first: get(pageSizeAtom),
|
|
||||||
after: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const compatSsoLoginListFamily = atomFamily((userId: string) => {
|
const compatSsoLoginListFamily = atomFamily((userId: string) => {
|
||||||
const compatSsoLoginList = atomWithQuery({
|
const compatSsoLoginList = atomWithQuery({
|
||||||
query: QUERY,
|
query: QUERY,
|
||||||
getVariables: (get) => ({ userId, ...get(currentPagination) }),
|
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return compatSsoLoginList;
|
return compatSsoLoginList;
|
||||||
@ -85,7 +86,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
|
|||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
const paginationFamily = atomFamily((userId: string) => {
|
||||||
const paginationAtom = atomWithPagination(
|
const paginationAtom = atomWithPagination(
|
||||||
currentPagination,
|
currentPaginationAtom,
|
||||||
pageInfoFamily(userId)
|
pageInfoFamily(userId)
|
||||||
);
|
);
|
||||||
return paginationAtom;
|
return paginationAtom;
|
||||||
@ -94,7 +95,7 @@ const paginationFamily = atomFamily((userId: string) => {
|
|||||||
const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
|
const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(compatSsoLoginListFamily(userId));
|
const result = useAtomValue(compatSsoLoginListFamily(userId));
|
||||||
const setPagination = useSetAtom(currentPagination);
|
const setPagination = useSetAtom(currentPaginationAtom);
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||||
|
|
||||||
const paginate = (pagination: Pagination) => {
|
const paginate = (pagination: Pagination) => {
|
||||||
|
@ -13,13 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { useAtomValue, atom, useSetAtom } from "jotai";
|
import { useAtomValue, atom, useSetAtom } from "jotai";
|
||||||
import { atomFamily, atomWithDefault } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { atomWithQuery } from "jotai-urql";
|
import { atomWithQuery } from "jotai-urql";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import { PageInfo } from "../gql/graphql";
|
import { PageInfo } from "../gql/graphql";
|
||||||
import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
|
import {
|
||||||
|
atomForCurrentPagination,
|
||||||
|
atomWithPagination,
|
||||||
|
Pagination,
|
||||||
|
} from "../pagination";
|
||||||
|
|
||||||
import BlockList from "./BlockList";
|
import BlockList from "./BlockList";
|
||||||
import OAuth2Session from "./OAuth2Session";
|
import OAuth2Session from "./OAuth2Session";
|
||||||
@ -61,15 +65,12 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const currentPagination = atomWithDefault<Pagination>((get) => ({
|
const currentPaginationAtom = atomForCurrentPagination();
|
||||||
first: get(pageSizeAtom),
|
|
||||||
after: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const oauth2SessionListFamily = atomFamily((userId: string) => {
|
const oauth2SessionListFamily = atomFamily((userId: string) => {
|
||||||
const oauth2SessionList = atomWithQuery({
|
const oauth2SessionList = atomWithQuery({
|
||||||
query: QUERY,
|
query: QUERY,
|
||||||
getVariables: (get) => ({ userId, ...get(currentPagination) }),
|
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return oauth2SessionList;
|
return oauth2SessionList;
|
||||||
@ -86,7 +87,7 @@ const pageInfoFamily = atomFamily((userId: string) => {
|
|||||||
|
|
||||||
const paginationFamily = atomFamily((userId: string) => {
|
const paginationFamily = atomFamily((userId: string) => {
|
||||||
const paginationAtom = atomWithPagination(
|
const paginationAtom = atomWithPagination(
|
||||||
currentPagination,
|
currentPaginationAtom,
|
||||||
pageInfoFamily(userId)
|
pageInfoFamily(userId)
|
||||||
);
|
);
|
||||||
return paginationAtom;
|
return paginationAtom;
|
||||||
@ -99,7 +100,7 @@ type Props = {
|
|||||||
const OAuth2SessionList: React.FC<Props> = ({ userId }) => {
|
const OAuth2SessionList: React.FC<Props> = ({ userId }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(oauth2SessionListFamily(userId));
|
const result = useAtomValue(oauth2SessionListFamily(userId));
|
||||||
const setPagination = useSetAtom(currentPagination);
|
const setPagination = useSetAtom(currentPaginationAtom);
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||||
|
|
||||||
const paginate = (pagination: Pagination) => {
|
const paginate = (pagination: Pagination) => {
|
||||||
|
@ -24,6 +24,7 @@ import Button from "./Button";
|
|||||||
import DateTime from "./DateTime";
|
import DateTime from "./DateTime";
|
||||||
import Input from "./Input";
|
import Input from "./Input";
|
||||||
import Typography, { Bold } from "./Typography";
|
import Typography, { Bold } from "./Typography";
|
||||||
|
import { emailPageResultFamily } from "./UserEmailList";
|
||||||
|
|
||||||
const FRAGMENT = graphql(/* GraphQL */ `
|
const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment UserEmail_email on UserEmail {
|
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 verifyEmailFamily = atomFamily((id: string) => {
|
||||||
const verifyEmail = atomWithMutation(VERIFY_EMAIL_MUTATION);
|
const verifyEmail = atomWithMutation(VERIFY_EMAIL_MUTATION);
|
||||||
|
|
||||||
@ -100,19 +113,35 @@ const resendVerificationEmailFamily = atomFamily((id: string) => {
|
|||||||
return resendVerificationEmailAtom;
|
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<{
|
const UserEmail: React.FC<{
|
||||||
email: FragmentType<typeof FRAGMENT>;
|
email: FragmentType<typeof FRAGMENT>;
|
||||||
|
userId: string; // XXX: Can we get this from the fragment?
|
||||||
isPrimary?: boolean;
|
isPrimary?: boolean;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}> = ({ email, isPrimary, highlight }) => {
|
}> = ({ email, userId, isPrimary, highlight }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const data = useFragment(FRAGMENT, email);
|
const data = useFragment(FRAGMENT, email);
|
||||||
const [verifyEmailResult, verifyEmail] = useAtom(verifyEmailFamily(data.id));
|
const [verifyEmailResult, verifyEmail] = useAtom(verifyEmailFamily(data.id));
|
||||||
const [resendVerificationEmailResult, resendVerificationEmail] = useAtom(
|
const [resendVerificationEmailResult, resendVerificationEmail] = useAtom(
|
||||||
resendVerificationEmailFamily(data.id)
|
resendVerificationEmailFamily(data.id)
|
||||||
);
|
);
|
||||||
|
const resetEmailPage = useSetAtom(emailPageResultFamily(userId));
|
||||||
|
const removeEmail = useSetAtom(removeEmailFamily(data.id));
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
// XXX: we probably want those callbacks passed in props instead
|
||||||
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.currentTarget);
|
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 =
|
const emailSent =
|
||||||
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
|
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
|
||||||
|
const invalidCode =
|
||||||
|
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block highlight={highlight}>
|
<Block highlight={highlight}>
|
||||||
@ -142,9 +183,16 @@ const UserEmail: React.FC<{
|
|||||||
Primary
|
Primary
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Typography variant="caption">
|
<div className="flex justify-between items-center">
|
||||||
<Bold>{data.email}</Bold>
|
<Typography variant="caption">
|
||||||
</Typography>
|
<Bold>{data.email}</Bold>
|
||||||
|
</Typography>
|
||||||
|
{!isPrimary && (
|
||||||
|
<Button disabled={pending} onClick={onRemoveClick} compact>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{data.confirmedAt ? (
|
{data.confirmedAt ? (
|
||||||
<Typography variant="micro">
|
<Typography variant="micro">
|
||||||
Verified <DateTime datetime={data.confirmedAt} />
|
Verified <DateTime datetime={data.confirmedAt} />
|
||||||
@ -162,6 +210,9 @@ const UserEmail: React.FC<{
|
|||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
/>
|
/>
|
||||||
|
{invalidCode && (
|
||||||
|
<div className="col-span-2 text-alert font-bold">Invalid code</div>
|
||||||
|
)}
|
||||||
<Button type="submit" disabled={pending}>
|
<Button type="submit" disabled={pending}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { atomFamily, atomWithDefault } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { atomWithQuery } from "jotai-urql";
|
import { atomWithQuery } from "jotai-urql";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ import { PageInfo } from "../gql/graphql";
|
|||||||
import {
|
import {
|
||||||
atomForCurrentPagination,
|
atomForCurrentPagination,
|
||||||
atomWithPagination,
|
atomWithPagination,
|
||||||
pageSizeAtom,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
} from "../pagination";
|
} from "../pagination";
|
||||||
|
|
||||||
@ -143,6 +142,7 @@ const UserEmailList: React.FC<{
|
|||||||
{result.data?.user?.emails?.edges?.map((edge) => (
|
{result.data?.user?.emails?.edges?.map((edge) => (
|
||||||
<UserEmail
|
<UserEmail
|
||||||
email={edge.node}
|
email={edge.node}
|
||||||
|
userId={userId}
|
||||||
key={edge.cursor}
|
key={edge.cursor}
|
||||||
isPrimary={primaryEmailId === edge.node.id}
|
isPrimary={primaryEmailId === edge.node.id}
|
||||||
highlight={highlightedEmail === edge.node.id}
|
highlight={highlightedEmail === edge.node.id}
|
||||||
|
@ -37,6 +37,8 @@ const documents = {
|
|||||||
types.VerifyEmailDocument,
|
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":
|
"\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,
|
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":
|
"\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,
|
types.UserEmailListQueryDocument,
|
||||||
"\n query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n":
|
"\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(
|
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"
|
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"];
|
): (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.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
@ -40,11 +40,11 @@ export type AddEmailInput = {
|
|||||||
export type AddEmailPayload = {
|
export type AddEmailPayload = {
|
||||||
__typename?: "AddEmailPayload";
|
__typename?: "AddEmailPayload";
|
||||||
/** The email address that was added */
|
/** The email address that was added */
|
||||||
email: UserEmail;
|
email?: Maybe<UserEmail>;
|
||||||
/** Status of the operation */
|
/** Status of the operation */
|
||||||
status: AddEmailStatus;
|
status: AddEmailStatus;
|
||||||
/** The user to whom the email address was added */
|
/** The user to whom the email address was added */
|
||||||
user: User;
|
user?: Maybe<User>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The status of the `addEmail` mutation */
|
/** The status of the `addEmail` mutation */
|
||||||
@ -53,6 +53,8 @@ export enum AddEmailStatus {
|
|||||||
Added = "ADDED",
|
Added = "ADDED",
|
||||||
/** The email address already exists */
|
/** The email address already exists */
|
||||||
Exists = "EXISTS",
|
Exists = "EXISTS",
|
||||||
|
/** The email address is invalid */
|
||||||
|
Invalid = "INVALID",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Anonymous = Node & {
|
export type Anonymous = Node & {
|
||||||
@ -178,6 +180,8 @@ export type Mutation = {
|
|||||||
__typename?: "Mutation";
|
__typename?: "Mutation";
|
||||||
/** Add an email address to the specified user */
|
/** Add an email address to the specified user */
|
||||||
addEmail: AddEmailPayload;
|
addEmail: AddEmailPayload;
|
||||||
|
/** Remove an email address */
|
||||||
|
removeEmail: RemoveEmailPayload;
|
||||||
/** Send a verification code for an email address */
|
/** Send a verification code for an email address */
|
||||||
sendVerificationEmail: SendVerificationEmailPayload;
|
sendVerificationEmail: SendVerificationEmailPayload;
|
||||||
/** Submit a verification code for an email address */
|
/** Submit a verification code for an email address */
|
||||||
@ -189,6 +193,11 @@ export type MutationAddEmailArgs = {
|
|||||||
input: AddEmailInput;
|
input: AddEmailInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The mutations root of the GraphQL interface. */
|
||||||
|
export type MutationRemoveEmailArgs = {
|
||||||
|
input: RemoveEmailInput;
|
||||||
|
};
|
||||||
|
|
||||||
/** The mutations root of the GraphQL interface. */
|
/** The mutations root of the GraphQL interface. */
|
||||||
export type MutationSendVerificationEmailArgs = {
|
export type MutationSendVerificationEmailArgs = {
|
||||||
input: SendVerificationEmailInput;
|
input: SendVerificationEmailInput;
|
||||||
@ -352,6 +361,33 @@ export type QueryUserEmailArgs = {
|
|||||||
id: Scalars["ID"];
|
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 */
|
/** The input for the `sendVerificationEmail` mutation */
|
||||||
export type SendVerificationEmailInput = {
|
export type SendVerificationEmailInput = {
|
||||||
/** The ID of the email address to verify */
|
/** The ID of the email address to verify */
|
||||||
@ -607,9 +643,13 @@ export type AddEmailMutation = {
|
|||||||
addEmail: {
|
addEmail: {
|
||||||
__typename?: "AddEmailPayload";
|
__typename?: "AddEmailPayload";
|
||||||
status: AddEmailStatus;
|
status: AddEmailStatus;
|
||||||
email: { __typename?: "UserEmail"; id: string } & {
|
email?:
|
||||||
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
|
| ({ __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<{
|
export type UserEmailListQueryQueryVariables = Exact<{
|
||||||
userId: Scalars["ID"];
|
userId: Scalars["ID"];
|
||||||
first?: InputMaybe<Scalars["Int"]>;
|
first?: InputMaybe<Scalars["Int"]>;
|
||||||
@ -2101,6 +2154,70 @@ export const ResendVerificationEmailDocument = {
|
|||||||
ResendVerificationEmailMutation,
|
ResendVerificationEmailMutation,
|
||||||
ResendVerificationEmailMutationVariables
|
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 = {
|
export const UserEmailListQueryDocument = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,11 @@ import { cacheExchange } from "@urql/exchange-graphcache";
|
|||||||
import { refocusExchange } from "@urql/exchange-refocus";
|
import { refocusExchange } from "@urql/exchange-refocus";
|
||||||
import { requestPolicyExchange } from "@urql/exchange-request-policy";
|
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";
|
import schema from "./gql/schema";
|
||||||
|
|
||||||
const cache = cacheExchange({
|
const cache = cacheExchange({
|
||||||
@ -39,6 +43,36 @@ const cache = cacheExchange({
|
|||||||
cache.invalidate(key, field.fieldName, field.arguments);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user