Deprecated API endpoints cleanup (#5478)

* Deprecated API endpoints cleanup

* Linting

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Omar López
2022-11-11 19:54:44 +00:00
committed by GitHub
co-authored by kodiakhq[bot]
parent fcde1f7a9d
commit abc5ff2d97
23 changed files with 9 additions and 1454 deletions
-119
View File
@@ -598,60 +598,6 @@ paths:
summary: Gets and stores the OAuth token
tags:
- Integrations
/api/user/profile:
patch:
description: Updates a user's profile.
summary: Updates a user's profile
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description:
type: string
avatar:
type: string
timeZone:
type: string
weekStart:
type: string
hideBranding:
type: string
theme:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
"500":
description: Internal Server Error
content:
application/json:
schema:
type: object
properties:
message:
type: string
/api/me:
get:
description: Gets current user's profile.
@@ -686,71 +632,6 @@ paths:
properties:
message:
type: string
/api/user/membership:
get:
description: Get a list of the teams the user has joined.
summary: Get the teams a user is joined to
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
"500":
description: Internal Server Error
content:
application/json:
schema:
type: object
properties:
message:
type: string
patch:
description: Accept team invitation
summary: Accept team invitation.
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
teamId:
type: string
delete:
description: Leave team or decline membership invite of current user
summary: Leave team or decline team invite.
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
teamId:
type: string
"/api/{team}":
delete:
description: Deletes a team
@@ -1,69 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
export default function DisconnectStripeIntegration(props: {
/** Integration credential id */
id: number;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const [modalOpen, setModalOpen] = useState(false);
const mutation = useMutation(
async (action: string) => {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: props.id, action }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Something went wrong");
}
return res.json();
},
{
async onSettled() {
await props.onOpenChange(modalOpen);
},
onSuccess(data) {
showToast(data.message, "success");
setModalOpen(false);
},
}
);
return (
<>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DeleteStripeDialogContent
variety="warning"
title="Disconnect Stripe Integration"
cancelAllBookingsBtnText="Cancel all bookings"
removeBtnText="Remove payment"
cancelBtnText="Cancel"
onConfirm={() => {
mutation.mutate("cancel");
}}
onRemove={() => {
mutation.mutate("remove");
}}>
If you have unpaid and unconfirmed bookings, you must choose to cancel them or remove the required
payment field.
</DeleteStripeDialogContent>
</Dialog>
{props.render({
onClick() {
setModalOpen(true);
},
disabled: modalOpen,
loading: mutation.isLoading,
})}
</>
);
}
+7
View File
@@ -0,0 +1,7 @@
# Before adding new endpoints here
We're deprecating adding new endpoints in favor of creating tRPC procedures.
You can learn about [tRPC procedures in the docs](https://trpc.io/docs/v10/procedures).
You can see our current tRPC procedures in this file `packages/trpc/server/routers/_app.ts`
-85
View File
@@ -1,85 +0,0 @@
import { PrismaClient } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type CalendlyEventType = {
name: string;
slug: string;
duration: number;
description_plain: string;
secret: boolean;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session?.user.id,
},
select: {
id: true,
},
});
if (req.method == "POST") {
const userResult = await fetch("https://api.calendly.com/users/me", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
if (userResult.status == 200) {
const userData = await userResult.json();
await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
name: userData.resource.name,
},
});
const eventTypesResult = await fetch(
"https://api.calendly.com/event_types?user=" + userData.resource.uri,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
}
);
const eventTypesData = await eventTypesResult.json();
eventTypesData.collection.forEach(async (eventType: CalendlyEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
slug: eventType.slug,
length: eventType.duration,
description: eventType.description_plain,
hidden: eventType.secret,
users: {
connect: {
id: authenticatedUser.id,
},
},
userId: authenticatedUser.id,
},
});
});
res.status(201).end();
} else {
res.status(500).end();
}
} else {
res.status(405).end();
}
}
-85
View File
@@ -1,85 +0,0 @@
import { PrismaClient } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type SavvyCalEventType = {
name: string;
slug: string;
durations: [number];
description: string;
state: "active";
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session?.user.id,
},
select: {
id: true,
},
});
if (req.method === "POST") {
const userResult = await fetch("https://api.savvycal.com/v1/me", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
if (userResult.status === 200) {
const userData = await userResult.json();
await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
name: userData.display_name,
timeZone: userData.time_zone,
weekStart: userData.first_day_of_week === 0 ? "Sunday" : "Monday",
avatar: userData.avatar_url,
},
});
const eventTypesResult = await fetch("https://api.savvycal.com/v1/links?limit=100", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
const eventTypesData = await eventTypesResult.json();
eventTypesData.entries.forEach(async (eventType: SavvyCalEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
slug: eventType.slug,
length: eventType.durations[0],
description: eventType.description.replace(/<[^>]*>?/gm, ""),
hidden: eventType.state === "active",
users: {
connect: {
id: authenticatedUser.id,
},
},
userId: authenticatedUser.id,
},
});
});
res.status(201).end();
} else {
res.status(500).end();
}
} else {
res.status(405).end();
}
}
-154
View File
@@ -1,154 +0,0 @@
import { BookingStatus, Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!["GET", "DELETE"].includes(req.method || "")) {
return res.status(405).end();
}
// Check that user is authenticated
const session = await getSession({ req });
const userId = session?.user?.id;
if (!userId) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
if (req.method === "GET") {
const credentials = await prisma.credential.findMany({
where: {
userId,
},
select: {
type: true,
},
});
res.status(200).json(credentials);
}
if (req.method == "DELETE") {
const id = req.body.id;
const data: Prisma.UserUpdateInput = {
credentials: {
delete: {
id,
},
},
};
const integration = await prisma.credential.findUnique({
where: {
id,
},
});
/* If the user deletes a zapier integration, we delete all his api keys as well. */
if (integration?.appId === "zapier") {
data.apiKeys = {
deleteMany: {
userId,
appId: "zapier",
},
};
/* We also delete all user's zapier wehbooks */
data.webhooks = {
deleteMany: {
userId,
appId: "zapier",
},
};
}
await prisma.user.update({
where: {
id: userId,
},
data,
});
if (req.body?.action === "cancel" || req.body?.action === "remove") {
try {
const bookingIdsWithPayments = await prisma.booking
.findMany({
where: {
userId: session?.user?.id,
paid: false,
NOT: {
payment: {
every: {
booking: null,
},
},
},
},
select: {
id: true,
},
})
.then((bookings) => bookings.map((booking) => booking.id));
const deletePayments = prisma.payment.deleteMany({
where: {
bookingId: {
in: bookingIdsWithPayments,
},
success: false,
},
});
const updateBookings = prisma.booking.updateMany({
where: {
id: {
in: bookingIdsWithPayments,
},
},
data: {
status: BookingStatus.CANCELLED,
rejectionReason: "Payment provider got removed",
},
});
const bookingReferences = await prisma.booking
.findMany({
where: {
status: BookingStatus.ACCEPTED,
},
select: {
id: true,
},
})
.then((bookings) => bookings.map((booking) => booking.id));
const deleteBookingReferences = prisma.bookingReference.deleteMany({
where: {
bookingId: {
in: bookingReferences,
},
},
});
if (req.body?.action === "cancel") {
await prisma.$transaction([deletePayments, updateBookings, deleteBookingReferences]);
} else {
const updateBookings = prisma.booking.updateMany({
where: {
id: {
in: bookingIdsWithPayments,
},
},
data: {
paid: true,
},
});
await prisma.$transaction([deletePayments, updateBookings]);
}
} catch (e) {
console.error(e);
res.status(500).json({ message: "Integration could not be deleted" });
}
}
res.status(200).json({ message: "Integration deleted successfully" });
}
}
@@ -1,48 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { defaultHandler } from "@calcom/lib/server";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { userMetadata as zodUserMetadata } from "@calcom/prisma/zod-utils";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { intentUsername } = req.body;
// Check that user is authenticated
try {
const session = await getSession({ req });
const userId = session?.user?.id;
const user = await prisma.user.findFirstOrThrow({
select: {
id: true,
metadata: true,
},
where: { id: userId },
});
const checkPremiumUsernameResult = await checkUsername(intentUsername);
if (userId && user) {
const userMetadata = zodUserMetadata.parse(user.metadata);
await prisma.user.update({
where: {
id: userId,
},
data: {
metadata: {
...userMetadata,
intentUsername,
isIntentPremium: checkPremiumUsernameResult.premium,
},
},
});
}
} catch (error) {
res.status(501).send({ message: "intent-username.save.error" });
}
res.end();
}
export default defaultHandler({
GET: Promise.resolve({ default: getHandler }),
});
-45
View File
@@ -1,45 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { defaultAvatarSrc } from "@lib/profile";
/**
* @deprecated Use TRCP's viewer.me query
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
bufferTime: true,
theme: true,
createdDate: true,
hideBranding: true,
avatar: true,
},
});
user.avatar = user.avatar || defaultAvatarSrc({ email: user.email });
res.status(200).json({
user,
});
}
-55
View File
@@ -1,55 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { getAvailabilityFromSchedule } from "@lib/availability";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const userId = session?.user?.id;
if (!userId) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (!req.body.schedule || req.body.schedule.length !== 7) {
return res.status(400).json({ message: "Bad Request." });
}
const availability = getAvailabilityFromSchedule(req.body.schedule);
if (req.method === "POST") {
try {
await prisma.user.update({
where: {
id: userId,
},
data: {
availability: {
/* We delete user availabilty */
deleteMany: {
userId: {
equals: userId,
},
},
/* So we can replace it */
createMany: {
data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
},
});
return res.status(200).json({
message: "created",
});
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to create schedule." });
}
}
}
-60
View File
@@ -1,60 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.log("Received session token without a user id.");
return res.status(500).json({ message: "Something went wrong." });
}
if (!req.query.team) {
console.log("Missing team query param.");
return res.status(500).json({ message: "Something went wrong." });
}
const teamId = parseInt(req.query.team as string);
// GET /api/teams/{team}
if (req.method === "GET") {
const team = await getTeamWithMembers(teamId);
return res.status(200).json({ team });
}
// DELETE /api/teams/{team}
if (req.method === "DELETE") {
const membership = await prisma.membership.findFirst({
where: {
userId: session.user.id,
teamId,
},
});
if (!membership || membership.role !== "OWNER") {
console.log(`User ${session.user.id} tried deleting an organization they don't own.`);
return res.status(403).json({ message: "Forbidden." });
}
await prisma.membership.delete({
where: {
userId_teamId: { userId: session.user.id, teamId },
},
});
const deletedTeam = await prisma.team.delete({
where: {
id: teamId,
},
});
// Sync Services: Close.com
closeComDeleteTeam(deletedTeam);
return res.status(204).send(null);
}
}
-132
View File
@@ -1,132 +0,0 @@
import { MembershipRole } from "@prisma/client";
import { randomBytes } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { sendTeamInviteEmail } from "@calcom/emails";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { BASE_URL } from "@lib/config/constants";
import slugify from "@lib/slugify";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");
if (req.method !== "POST") {
return res.status(400).json({ message: "Bad request" });
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
const team = await prisma.team.findFirst({
where: {
id: parseInt(req.query.team as string),
},
});
if (!team) {
return res.status(404).json({ message: "Invalid team" });
}
const reqBody = req.body as {
usernameOrEmail: string;
role: MembershipRole;
sendEmailInvitation: boolean;
};
const { role, sendEmailInvitation } = reqBody;
// liberal email match
const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
const usernameOrEmail = isEmail(reqBody.usernameOrEmail)
? reqBody.usernameOrEmail.toLowerCase()
: slugify(reqBody.usernameOrEmail);
const invitee = await prisma.user.findFirst({
where: {
OR: [{ username: usernameOrEmail }, { email: usernameOrEmail }],
},
});
if (!invitee) {
const email = isEmail(usernameOrEmail) ? usernameOrEmail : undefined;
if (!email) {
return res.status(400).json({
message: `Invite failed because there is no corresponding user for ${usernameOrEmail}`,
});
}
await prisma.user.create({
data: {
email,
teams: {
create: {
team: {
connect: {
id: parseInt(req.query.team as string),
},
},
role,
},
},
},
});
const token: string = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,
expires: new Date(new Date().setHours(168)), // +1 week
},
});
if (session?.user?.name && team?.name) {
await sendTeamInviteEmail({
language: t,
from: session.user.name,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=/settings/teams}`,
});
}
return res.status(201).json({});
}
// create provisional membership
try {
await prisma.membership.create({
data: {
teamId: parseInt(req.query.team as string),
userId: invitee.id,
role,
},
});
} catch (err: any) {
if (err.code === "P2002") {
// unique constraint violation
return res.status(409).json({
message: "This user is a member of this team / has a pending invitation.",
});
} else {
throw err; // rethrow
}
}
// inform user of membership by email
if (sendEmailInvitation && session?.user?.name && team?.name) {
await sendTeamInviteEmail({
language: t,
from: session.user.name,
to: usernameOrEmail,
teamName: team.name,
joinLink: BASE_URL + "/settings/teams",
});
}
res.status(201).json({});
}
@@ -1,86 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const isTeamOwner = !!(await prisma.membership.findFirst({
where: {
userId: session.user?.id,
teamId: parseInt(req.query.team as string),
role: "OWNER",
},
}));
if (!isTeamOwner) {
res.status(403).json({ message: "You are not authorized to manage this team" });
return;
}
// List members
if (req.method === "GET") {
const memberships = await prisma.membership.findMany({
where: {
teamId: parseInt(req.query.team as string),
},
});
let members = await prisma.user.findMany({
where: {
id: {
in: memberships.map((membership) => membership.userId),
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
timeZone: true,
},
});
members = members.map((member) => {
const membership = memberships.find((membership) => member.id === membership.userId);
return {
...member,
role: membership?.accepted ? membership?.role : "INVITEE",
};
});
return res.status(200).json({ members: members });
}
// Cancel a membership (invite)
if (req.method === "DELETE") {
const user = await prisma.user.findFirst({
where: {
id: req.body.userId,
},
});
await prisma.membership.delete({
where: {
userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team as string) },
},
});
// Sync Services: Close.com
closeComDeleteTeamMembership(user);
return res.status(204).send(null);
}
// Promote or demote a member of the team
res.status(200).json({});
}
@@ -1,71 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
// @deprecated - USE TRPC
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
const isTeamOwner = !!(await prisma.membership.findFirst({
where: {
userId: session.user.id,
teamId: parseInt(req.query.team as string),
role: "OWNER",
},
}));
if (!isTeamOwner) {
res.status(403).json({ message: "You are not authorized to manage this team" });
return;
}
// PATCH /api/teams/profile/{team}
if (req.method === "PATCH") {
const team = await prisma.team.findFirst({
where: {
id: parseInt(req.query.team as string),
},
});
if (!team) {
return res.status(404).json({ message: "Invalid team" });
}
const username = req.body.username;
const userConflict = await prisma.team.findMany({
where: {
slug: username,
},
});
const teamId = Number(req.query.team);
if (userConflict.some((team) => team.id !== teamId)) {
return res.status(409).json({ message: "Team username already taken" });
}
const name = req.body.name;
const slug = req.body.username;
const bio = req.body.description;
const logo = req.body.logo;
const hideBranding = req.body.hideBranding;
await prisma.team.update({
where: {
id: team.id,
},
data: {
name,
slug,
logo,
bio,
hideBranding,
},
});
return res.status(200).json({ message: "Team updated successfully" });
}
}
-54
View File
@@ -1,54 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getStripeCustomerId } from "@calcom/app-store/stripepayment/lib/customer";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { WEBSITE_URL } from "@lib/config/constants";
import { HttpError as HttpCode } from "@lib/core/http/error";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!["GET", "POST"].includes(req.method || "")) {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user.id,
},
select: {
email: true,
metadata: true,
},
});
const stripeCustomerId = await getStripeCustomerId(user);
try {
const response = await fetch(`${WEBSITE_URL}/api/upgrade`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
stripeCustomerId,
email: user.email,
fromApp: true,
}),
});
const data = await response.json();
if (!data.url) throw new HttpCode({ statusCode: 401, message: data.message });
res.redirect(303, data.url);
} catch (error) {
console.error(`error`, error);
res.redirect(303, req.headers.origin || "/");
}
}
-86
View File
@@ -1,86 +0,0 @@
import { pick } from "lodash";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user.id) {
return res.status(401).json({ message: "Not authenticated" });
}
const querySchema = z.object({
id: z.string().transform((val) => parseInt(val)),
});
const parsedQuery = querySchema.safeParse(req.query);
const userId = parsedQuery.success ? parsedQuery.data.id : null;
if (!userId) {
return res.status(400).json({ message: "No user id provided" });
}
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session.user.id,
},
select: {
id: true,
},
});
if (userId !== authenticatedUser.id) {
return res.status(401).json({ message: "Unauthorized" });
}
if (req.method === "GET") {
return res.status(405).json({ message: "Method Not Allowed" });
}
if (req.method === "PATCH") {
const updatedUser = await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
...pick(req.body.data, [
"username",
"name",
"avatar",
"timeZone",
"timeFormat",
"weekStart",
"hideBranding",
"theme",
"completedOnboarding",
]),
bio: req.body.description ?? req.body.data?.bio,
},
select: {
id: true,
username: true,
name: true,
email: true,
emailVerified: true,
bio: true,
avatar: true,
timeZone: true,
timeFormat: true,
weekStart: true,
startTime: true,
endTime: true,
bufferTime: true,
hideBranding: true,
theme: true,
createdDate: true,
plan: true,
completedOnboarding: true,
},
});
return res.status(200).json({ message: "User Updated", data: updatedUser });
}
}
-50
View File
@@ -1,50 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (req.method !== "DELETE") {
return res.status(405).json({ message: "Method Not Allowed" });
}
if (req.method === "DELETE") {
// Get user
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user?.id,
},
select: {
id: true,
email: true,
metadata: true,
username: true,
createdDate: true,
name: true,
plan: true,
},
});
// Delete from stripe
await deleteStripeCustomer(user).catch(console.warn);
// Delete from Cal
await prisma.user.delete({
where: {
id: session?.user.id,
},
});
// Sync Services
syncServicesDeleteWebUser(user);
return res.status(204).end();
}
}
-63
View File
@@ -1,63 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session || !session.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (req.method === "GET") {
const memberships = await prisma.membership.findMany({
where: {
userId: session.user.id,
},
});
const teams = await prisma.team.findMany({
where: {
id: {
in: memberships.map((membership) => membership.teamId),
},
},
});
return res.status(200).json({
membership: memberships.map((membership) => ({
role: membership.accepted ? membership.role : "INVITEE",
...teams.find((team) => team.id === membership.teamId),
})),
});
}
if (!req.body.teamId) {
return res.status(400).json({ message: "Bad request" });
}
// Leave team or decline membership invite of current user
if (req.method === "DELETE") {
await prisma.membership.delete({
where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
},
});
return res.status(204).send(null);
}
// Accept team invitation
if (req.method === "PATCH") {
await prisma.membership.update({
where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
},
data: {
accepted: true,
},
});
return res.status(204).send(null);
}
}
-50
View File
@@ -1,50 +0,0 @@
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { pick } from "lodash";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
try {
const avatar = req.body.avatar ? await resizeBase64Image(req.body.avatar) : undefined;
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
...pick(req.body, [
"username",
"name",
"timeZone",
"weekStart",
"hideBranding",
"theme",
"completedOnboarding",
"locale",
]),
avatar,
bio: req.body.description,
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2002") {
return res.status(409).json({ message: "Username already taken" });
}
}
throw e;
}
return res.status(200).json({ message: "Profile updated successfully" });
}
-28
View File
@@ -1,28 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { ensureSession } from "@calcom/lib/auth";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
// Only logged in users can opt-in/out
await ensureSession({ req });
// If has the cookie, Opt-out of V2
if ("calcom-v2-early-access" in req.cookies && req.cookies["calcom-v2-early-access"] === "1") {
res.setHeader("Set-Cookie", `calcom-v2-early-access=0; Max-Age=0; Path=/`);
} else {
/* Opt-int to V2 */
res.setHeader("Set-Cookie", "calcom-v2-early-access=1; Path=/");
}
let redirectUrl = "/";
// We take you back where you came from if possible
if (typeof req.headers["referer"] === "string") redirectUrl = req.headers["referer"];
res.redirect(redirectUrl);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
-10
View File
@@ -52,16 +52,6 @@ const BillingView = () => {
<>
<Meta title={t("billing")} description={t("manage_billing_description")} />
<div className="space-y-6 text-sm sm:space-y-8">
{!isPro && (
<CtaRow title={t("billing_freeplan_title")} description={t("billing_freeplan_description")}>
<form target="_blank" method="POST" action="/api/upgrade">
<Button type="submit" EndIcon={Icon.FiExternalLink}>
{t("billing_freeplan_cta")}
</Button>
</form>
</CtaRow>
)}
<CtaRow
className={classNames(!isPro && "pointer-events-none opacity-30")}
title={t("billing_manage_details_title")}
@@ -56,17 +56,8 @@ export default class LarkCalendarService implements Calendar {
const refreshExpireDate = larkAuthCredentials.refresh_expires_date;
const refreshToken = larkAuthCredentials.refresh_token;
if (isExpired(refreshExpireDate) || !refreshToken) {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: credential.id }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("disconnection wrong");
}
throw new Error("refresh token expires");
await prisma.credential.delete({ where: { id: credential.id } });
throw new Error("Lark Calendar refresh token expired");
}
try {
const appAccessToken = await getAppAccessToken();
@@ -1,39 +0,0 @@
import dayjs from "@calcom/dayjs";
import { TRIAL_LIMIT_DAYS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import Button from "@calcom/ui/Button";
const TrialBanner = () => {
const { t } = useLocale();
const query = useMeQuery();
const user = query.data;
if (!user || user.plan !== "TRIAL") return null;
const trialDaysLeft = user.trialEndsAt
? dayjs(user.trialEndsAt).add(1, "day").diff(dayjs(), "day")
: dayjs(user.createdDate)
.add(TRIAL_LIMIT_DAYS + 1, "day")
.diff(dayjs(), "day");
return (
<div
className="m-4 hidden rounded-md bg-yellow-200 p-4 text-center text-sm font-medium text-gray-600 lg:block"
data-testid="trial-banner">
<div className="mb-2 text-left">
{trialDaysLeft > 0 ? t("trial_days_left", { days: trialDaysLeft }) : t("trial_expired")}
</div>
<Button
href="/api/upgrade"
color="minimal"
prefetch={false}
className="w-full justify-center border-2 border-gray-600 hover:bg-yellow-100">
{t("upgrade_now")}
</Button>
</div>
);
};
export default TrialBanner;
-54
View File
@@ -1,54 +0,0 @@
import { Trans } from "next-i18next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "./Button";
import { Dialog, DialogClose, DialogContent } from "./Dialog";
import { Icon } from "./Icon";
export function UpgradeToProDialog({
modalOpen,
setModalOpen,
children,
}: {
modalOpen: boolean;
setModalOpen: (open: boolean) => void;
children: React.ReactNode;
}) {
const { t } = useLocale();
return (
<Dialog open={modalOpen}>
<DialogContent>
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
<Icon.FiAlertTriangle className="h-6 w-6 text-yellow-400" aria-hidden="true" />
</div>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("only_available_on_pro_plan")}
</h3>
</div>
</div>
<div className="flex flex-col space-y-3">
<p>{children}</p>
<p>
<Trans i18nKey="plan_upgrade_instructions">
You can
<a href="/api/upgrade" className="underline">
upgrade here
</a>
.
</Trans>
</p>
</div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild>
<Button className="table-cell w-full text-center" onClick={() => setModalOpen(false)}>
{t("dismiss")}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
);
}