fix: error when unlinking google account (#15011)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
parent
51428087ef
commit
e030fe0e07
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user