fix: error when unlinking google account (#15011)

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
Omar López
2024-05-14 10:10:41 -07:00
committed by GitHub
co-authored by Carina Wollendorfer
parent 51428087ef
commit e030fe0e07
7 changed files with 140 additions and 26 deletions
+10 -4
View File
@@ -124,6 +124,15 @@ const ProfileView = () => {
}
},
});
const unlinkConnectedAccountMutation = trpc.viewer.unlinkConnectedAccount.useMutation({
onSuccess: async (res) => {
showToast(t(res.message), "success");
utils.viewer.me.invalidate();
},
onError: (e) => {
showToast(t(e.message), "error");
},
});
const addSecondaryEmailMutation = trpc.viewer.addSecondaryEmail.useMutation({
onSuccess: (res) => {
@@ -439,10 +448,7 @@ const ProfileView = () => {
<Button
color="primary"
onClick={() => {
updateProfileMutation.mutate({
...tempFormValues,
unlinkConnectedAccount: true,
});
unlinkConnectedAccountMutation.mutate();
setShowAccountDisconnectWarning(false);
}}>
{t("confirm")}
@@ -2335,6 +2335,8 @@
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
"account_already_linked": "Account is already linked",
"account_unlinked_success": "Account unlinked successfully",
"account_unlinked_error": "There was an error unlinking the account",
"travel_schedule": "Travel Schedule",
"travel_schedule_description": "Plan your trip ahead to keep your existing schedule in a different timezone and prevent being booked at midnight.",
"schedule_timezone_change": "Schedule timezone change",
@@ -1,5 +1,5 @@
import authedProcedure from "../../procedures/authedProcedure";
import { router } from "../../trpc";
import { importHandler, router } from "../../trpc";
import { ZAddSecondaryEmailInputSchema } from "./addSecondaryEmail.schema";
import { ZAppByIdInputSchema } from "./appById.schema";
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
@@ -22,6 +22,10 @@ import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
import { ZWorkflowOrderInputSchema } from "./workflowOrder.schema";
const NAMESPACE = "loggedInViewer";
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
type AppsRouterHandlerCache = {
me?: typeof import("./me.handler").meHandler;
shouldVerifyEmail?: typeof import("./shouldVerifyEmail.handler").shouldVerifyEmailHandler;
@@ -192,6 +196,14 @@ export const loggedInViewerRouter = router({
return UNSTABLE_HANDLER_CACHE.updateProfile({ ctx, input });
}),
unlinkConnectedAccount: authedProcedure.mutation(async (opts) => {
const handler = await importHandler(
namespaced("unlinkConnectedAccount"),
() => import("./unlinkConnectedAccount.handler")
);
return handler(opts);
}),
eventTypeOrder: authedProcedure.input(ZEventTypeOrderInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.eventTypeOrder) {
UNSTABLE_HANDLER_CACHE.eventTypeOrder = (
@@ -0,0 +1,71 @@
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { IdentityProvider } from "@calcom/prisma/enums";
import unlinkConnectedAccountHandler from "./unlinkConnectedAccount.handler";
const buildOrgMockData = () => ({ id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null });
const buildProfileMockData = () => ({
username: "test",
upId: "usr-xx",
id: null,
organizationId: null,
organization: null,
name: "Test User",
avatarUrl: null,
startTime: 0,
endTime: 1440,
bufferTime: 0,
});
async function buildMockData(
identityProvider: IdentityProvider = IdentityProvider.GOOGLE,
identityProviderId: string | null = null
) {
const promise = await prismock.user.create({
data: {
id: 1,
username: "test",
name: "Test User",
email: "test@example.com",
identityProvider,
identityProviderId,
},
});
const user = await promise;
return {
...user,
organization: buildOrgMockData(),
defaultBookerLayouts: null,
selectedCalendars: [],
destinationCalendar: null,
profile: buildProfileMockData(),
avatar: "",
locale: "en",
};
}
describe("unlinkConnectedAccount.handler", () => {
it("Should response with a success message when unlinking an Google account", async () => {
const user = await buildMockData(IdentityProvider.GOOGLE, "123456789012345678901");
const response = await unlinkConnectedAccountHandler({ ctx: { user } });
expect(response).toMatchInlineSnapshot(`
{
"message": "account_unlinked_success",
}
`);
});
it("Should respond with an error message if unlink was unsucessful", async () => {
const user = await buildMockData(IdentityProvider.CAL);
const response = await unlinkConnectedAccountHandler({ ctx: { user } });
expect(response).toMatchInlineSnapshot(`
{
"message": "account_unlinked_error",
}
`);
});
});
@@ -0,0 +1,44 @@
import type { GetServerSidePropsContext, NextApiResponse } from "next";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type UpdateProfileOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
res?: NextApiResponse | GetServerSidePropsContext["res"];
};
};
const unlinkConnectedAccount = async ({ ctx }: UpdateProfileOptions) => {
const { user } = ctx;
// Unlink the account
const CalComAdapter = (await import("@calcom/features/auth/lib/next-auth-custom-adapter")).default;
const calcomAdapter = CalComAdapter(prisma);
// If it fails to delete, don't stop because the users login data might not be present
try {
await calcomAdapter.unlinkAccount({
provider: user.identityProvider.toLocaleLowerCase(),
providerAccountId: user.identityProviderId || "",
});
} catch {
// Fail silenty if we don't have an record in the account table
}
// Fall back to the default identity provider
const _user = await prisma.user.update({
where: {
id: user.id,
identityProvider: IdentityProvider.GOOGLE,
identityProviderId: { not: null },
},
data: {
identityProvider: IdentityProvider.CAL,
identityProviderId: null,
},
});
if (!_user) return { message: "account_unlinked_error" };
return { message: "account_unlinked_success" };
};
export default unlinkConnectedAccount;
@@ -19,7 +19,6 @@ import slugify from "@calcom/lib/slugify";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -74,9 +73,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
const secondaryEmails = input?.secondaryEmails || [];
delete input.secondaryEmails;
const unlinkConnectedAccount = input?.unlinkConnectedAccount || false;
delete input.unlinkConnectedAccount;
const data: Prisma.UserUpdateInput = {
...rest,
metadata: userMetadata,
@@ -180,22 +176,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
}
}
if (unlinkConnectedAccount) {
// Unlink the account
const CalComAdapter = (await import("@calcom/features/auth/lib/next-auth-custom-adapter")).default;
const calcomAdapter = CalComAdapter(prisma);
// If it fails to delete, don't stop because the users login data might not be present
try {
await calcomAdapter.unlinkAccount({
provider: user.identityProvider.toLocaleLowerCase(),
providerAccountId: user.identityProviderId || "",
});
} catch {}
// Only validate if we're changing email
data.identityProvider = IdentityProvider.CAL;
data.identityProviderId = null;
}
// if defined AND a base 64 string, upload and update the avatar URL
if (input.avatarUrl && input.avatarUrl.startsWith("data:image/png;base64,")) {
data.avatarUrl = await uploadAvatar({
@@ -48,7 +48,6 @@ export const ZUpdateProfileInputSchema = z.object({
})
)
.optional(),
unlinkConnectedAccount: z.boolean().optional(),
});
export type TUpdateProfileInputSchema = z.infer<typeof ZUpdateProfileInputSchema>;