chore: move miscellaneous small api pages to app dir api (#19699)

* version and verify-booking-token

* username route

* api/me

* logo route

* render email in dev

* link route

* future

* nope and geo location

* referal link and daily

* intercom route and dynamic variables

* intercom route and dynamic variables

* scim 2.0 and helpscout route

* app credentials

* api/book events

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* Revert "api/book events"

This reverts commit 607a32fb5b754cad090c2d0cbf64a68f990e220e.

* mock next server NextResponse to fix daily video

* add default responder to teams api

* fix search params

* uses nextUrl.searchParams instead of new url

* uses nextUrl.searchParams instead of new url

* remove outdated api config

* remove app dir version of inbound dynamic variables

* restore pages version of inbound variables

* fix missing code from stupid cursor

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
sean-brydon
2025-03-09 11:56:56 -04:00
committed by GitHub
co-authored by Benny Joo
parent 1f1862e526
commit d1ce04edc1
41 changed files with 1206 additions and 938 deletions
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
@@ -10,19 +11,16 @@ import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60];
let notificationsSent = 0;
for (const interval of reminderIntervalMinutes) {
const bookings = await prisma.booking.findMany({
where: {
@@ -144,5 +142,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
notificationsSent++;
}
}
res.status(200).json({ notificationsSent });
return NextResponse.json({ notificationsSent });
}
@@ -0,0 +1,23 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import prisma from "@calcom/prisma";
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
expiresAt: {
lte: new Date(Date.now()),
},
},
});
return NextResponse.json({ ok: true, count: deleted.count });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
@@ -19,16 +20,11 @@ const travelScheduleSelect = {
},
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
if (process.env.CRON_API_KEY !== apiKey) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
let timeZonesChanged = 0;
@@ -169,5 +165,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
res.status(200).json({ timeZonesChanged });
return NextResponse.json({ timeZonesChanged });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { TeamBilling } from "@calcom/ee/billing/teams";
@@ -8,20 +9,17 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
let { page: pageNumber } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
while (true) {
const teams = await prisma.team.findMany({
@@ -51,5 +49,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNumber++;
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
@@ -1,5 +1,6 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
@@ -12,22 +13,17 @@ const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
let { page: pageNumber } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
const firstDateOfMonth = new Date();
firstDateOfMonth.setDate(1);
@@ -314,5 +310,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNumber++;
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import logger from "@calcom/lib/logger";
@@ -14,15 +15,11 @@ const log = logger.getSubLogger({
* syncAppMeta makes sure any app metadata that has been replicated into the database
* remains synchronized with any changes made to the app config files.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
log.info(`🧐 Checking DB apps are in-sync with app metadata`);
@@ -63,5 +60,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
res.json({ ok: true });
return NextResponse.json({ ok: true });
}
+75
View File
@@ -0,0 +1,75 @@
import { NextResponse } from "next/server";
import { renderEmail } from "@calcom/emails";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
/**
* This API endpoint is used for development purposes to preview email templates
*/
export async function GET() {
// Only allow in development mode
if (IS_PRODUCTION) {
return new NextResponse("Only for development purposes", {
status: 403,
});
}
const t = await getTranslation("en", "common");
// Render the email template
const emailHtml = await renderEmail("MonthlyDigestEmail", {
language: t,
Created: 12,
Completed: 13,
Rescheduled: 14,
Cancelled: 16,
mostBookedEvents: [
{
eventTypeId: 3,
eventTypeName: "Test1",
count: 3,
},
{
eventTypeId: 4,
eventTypeName: "Test2",
count: 5,
},
],
membersWithMostBookings: [
{
userId: 4,
user: {
id: 4,
name: "User1 name",
email: "email.com",
avatar: "none",
username: "User1",
},
count: 4,
},
{
userId: 6,
user: {
id: 6,
name: "User2 name",
email: "email2.com",
avatar: "none",
username: "User2",
},
count: 8,
},
],
admin: { email: "admin.com", name: "admin" },
team: { name: "Team1", id: 4 },
});
// Create a response with the HTML content
const response = new NextResponse(emailHtml);
// Set appropriate headers
response.headers.set("Content-Type", "text/html");
response.headers.set("Cache-Control", "no-cache, no-store, private, must-revalidate");
return response;
}
+37
View File
@@ -0,0 +1,37 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FUTURE_ROUTES_OVERRIDE_COOKIE_NAME as COOKIE_NAME } from "@calcom/lib/constants";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session || !session.user || !session.user.email) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
}
let redirectUrl = "/";
// We take you back where you came from if possible
const referer = headersList.get("referer");
if (referer) redirectUrl = referer;
const response = NextResponse.redirect(redirectUrl);
// If has the cookie, Opt-out of V2
if (cookiesList.has(COOKIE_NAME) && cookiesList.get(COOKIE_NAME)?.value === "1") {
response.cookies.set(COOKIE_NAME, "0", { maxAge: 0, path: "/" });
} else {
/* Opt-in to V2 */
response.cookies.set(COOKIE_NAME, "1", { path: "/" });
}
return response;
}
@@ -1,25 +1,28 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { dub } from "@calcom/features/auth/lib/dub";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const session = await getServerSession({ req });
export async function POST() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session?.user?.username) {
return res.status(401).json({ message: "Unauthorized" });
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
try {
const { shortLink } = await dub.links.get({
externalId: `ext_${session.user.id.toString()}`,
});
return res.status(200).json({ shortLink });
return NextResponse.json({ shortLink });
} catch (error) {
console.log("Referral link not found, creating...");
}
@@ -46,5 +49,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
return res.status(200).json({ shortLink });
return NextResponse.json({ shortLink });
}
+12
View File
@@ -0,0 +1,12 @@
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const headersList = headers();
const country = headersList.get("x-vercel-ip-country") || "Unknown";
const response = NextResponse.json({ country });
response.headers.set("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400");
return response;
}
+30
View File
@@ -0,0 +1,30 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import crypto from "node:crypto";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function GET() {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
const secret = process.env.INTERCOM_SECRET;
if (!session) {
return NextResponse.json({ message: "user not authenticated" }, { status: 401 });
}
if (!secret) {
return NextResponse.json({ message: "Intercom Identity Verification secret not set" }, { status: 400 });
}
const hmac = crypto.createHmac("sha256", secret);
hmac.update(String(session.user.id));
const hash = hmac.digest("hex");
return NextResponse.json({ hash });
}
+112
View File
@@ -0,0 +1,112 @@
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import type { UserProfile } from "@calcom/types/UserProfile";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
reason: z.string().optional(),
});
const decryptedSchema = z.object({
bookingUid: z.string(),
userId: z.number().int(),
});
// Move the sessionGetter function outside the GET function
const createSessionGetter = (userId: number) => async () => {
return {
user: {
id: userId,
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
};
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const searchParams = request.nextUrl.searchParams;
try {
const { action, token, reason } = querySchema.parse(Object.fromEntries(searchParams.entries()));
const decryptedData = JSON.parse(
symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || "")
);
const { bookingUid, userId } = decryptedSchema.parse(decryptedData);
const booking = await prisma.booking.findUniqueOrThrow({
where: { uid: bookingUid },
});
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
});
// Use the factory function instead of declaring inside the block
const sessionGetter = createSessionGetter(userId);
try {
/** @see https://trpc.io/docs/server-side-calls */
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const res = {} as any; // Response is still mocked as it's not used in this context
const ctx = await createContext({ req: legacyReq, res }, sessionGetter);
const caller = bookingsRouter.createCaller({
...ctx,
req: legacyReq,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent(message)}`
);
}
return NextResponse.redirect(`${url.origin}/booking/${bookingUid}`);
} catch (error) {
console.error("Error processing link request:", error);
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
}
@@ -1,4 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -16,6 +18,8 @@ import {
} from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const log = logger.getSubLogger({ prefix: ["[api/logo]"] });
function removePort(url: string) {
@@ -123,10 +127,17 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
where: {
slug: subdomain,
...(isValidOrgDomain && {
metadata: {
path: ["isOrganization"],
equals: true,
},
OR: [
{
metadata: {
path: ["isOrganization"],
equals: true,
},
},
{
isOrganization: true,
},
],
}),
},
select: {
@@ -151,15 +162,23 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
/**
* This API endpoint is used to serve the logo associated with a team if no logo is found we serve our default logo
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { query } = req;
const parsedQuery = logoApiSchema.parse(query);
const { isValidOrgDomain } = orgDomainConfig(req);
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const parsedQuery = logoApiSchema.parse(Object.fromEntries(searchParams.entries()));
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const { isValidOrgDomain } = orgDomainConfig(legacyReq);
const hostname = request.headers.get("host");
if (!hostname) {
return NextResponse.json({ error: "No hostname" }, { status: 400 });
}
const hostname = req?.headers["host"];
if (!hostname) throw new Error("No hostname");
const domains = extractSubdomainAndDomain(hostname);
if (!domains) throw new Error("No domains");
if (!domains) {
return NextResponse.json({ error: "No domains" }, { status: 400 });
}
const [subdomain] = domains;
const teamLogos = await getTeamLogos(subdomain, isValidOrgDomain);
@@ -186,11 +205,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
// Create a new response with the image buffer
const imageResponse = new NextResponse(buffer);
// Set the appropriate headers
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
return imageResponse;
} catch (error) {
res.statusCode = 404;
res.json({ error: "Failed fetching logo" });
return NextResponse.json({ error: "Failed fetching logo" }, { status: 404 });
}
}
@@ -1,25 +1,36 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { performance } from "@calcom/lib/server/perfObserver";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
let isCold = true;
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
export async function GET() {
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
const session = await getServerSession({ req });
if (!session) return res.status(409).json({ message: "Unauthorized" });
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(headers(), cookies());
const session = await getServerSession({ req: legacyReq });
if (!session) {
return NextResponse.json({ message: "Unauthorized" }, { status: 409 });
}
const preUserDate = performance.now();
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) return res.status(404).json({ message: "No user found" });
if (!user) {
return NextResponse.json({ message: "No user found" }, { status: 404 });
}
const lastUpdate = performance.now();
res.setHeader("x-is-cold", isCold.toString());
isCold = false;
return res.status(200).json({
// Create response with headers
const response = NextResponse.json({
message: `Hello ${user.name}`,
prePrismaDate,
prismaDuration: `Prisma took ${preSessionDate - prePrismaDate}ms`,
@@ -30,4 +41,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
lastUpdate,
wasCold: isCold,
});
// Set custom header
response.headers.set("x-is-cold", isCold.toString());
isCold = false;
return response;
}
+9
View File
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
export async function POST() {
return NextResponse.json({ message: "Please don't" }, { status: 400 });
}
@@ -1,5 +1,7 @@
import { createHmac } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
@@ -8,7 +10,6 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import {
getAllTranscriptsAccessLinkFromMeetingId,
getDownloadLinkOfCalVideoByRecordingId,
@@ -32,11 +33,7 @@ import {
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
const computeSignature = (
hmacSecret: string,
reqBody: NextApiRequest["body"],
webhookTimestampHeader: string | string[] | undefined
) => {
const computeSignature = (hmacSecret: string, reqBody: any, webhookTimestampHeader: string | null) => {
const signature = `${webhookTimestampHeader}.${JSON.stringify(reqBody)}`;
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
const hmac = createHmac("sha256", base64DecodedSecret);
@@ -51,53 +48,53 @@ const getDownloadLinkOfCalVideo = async (recordingId: string) => {
return downloadLink;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
export async function POST(request: NextRequest) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" });
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
}
if (testRequestSchema.safeParse(req.body).success) {
return res.status(200).json({ message: "Test request successful" });
const body = await request.json();
if (testRequestSchema.safeParse(body).success) {
return NextResponse.json({ message: "Test request successful" });
}
const headersList = headers();
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE;
if (!testMode) {
const hmacSecret = process.env.DAILY_WEBHOOK_SECRET;
if (!hmacSecret) {
return res.status(405).json({ message: "No Daily Webhook Secret" });
return NextResponse.json({ message: "No Daily Webhook Secret" }, { status: 405 });
}
const computed_signature = computeSignature(hmacSecret, req.body, req.headers["x-webhook-timestamp"]);
const webhookTimestamp = headersList.get("x-webhook-timestamp");
const computed_signature = computeSignature(hmacSecret, body, webhookTimestamp);
if (req.headers["x-webhook-signature"] !== computed_signature) {
return res.status(403).json({ message: "Signature does not match" });
if (headersList.get("x-webhook-signature") !== computed_signature) {
return NextResponse.json({ message: "Signature does not match" }, { status: 403 });
}
}
log.debug(
"Daily video webhook Request Body:",
safeStringify({
body: req.body,
body,
})
);
try {
if (req.body?.type === "recording.ready-to-download") {
const recordingReadyResponse = recordingReadySchema.safeParse(req.body);
if (body?.type === "recording.ready-to-download") {
const recordingReadyResponse = recordingReadySchema.safeParse(body);
if (!recordingReadyResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { room_name, recording_id, status } = recordingReadyResponse.data.payload;
if (status !== "finished") {
return res.status(400).send({
message: "Recording not finished",
});
return NextResponse.json({ message: "Recording not finished" }, { status: 400 });
}
const bookingReference = await getBookingReference(room_name);
@@ -138,19 +135,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Submit Transcription Batch Processor Job
await submitBatchProcessorTranscriptionJob(recording_id);
} catch (err) {
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
}
// send emails to all attendees only when user has team plan
await sendDailyVideoRecordingEmails(evt, downloadLink);
return res.status(200).json({ message: "Success" });
} else if (req.body.type === "meeting.ended") {
const meetingEndedResponse = meetingEndedSchema.safeParse(req.body);
return NextResponse.json({ message: "Success" });
} else if (body.type === "meeting.ended") {
const meetingEndedResponse = meetingEndedSchema.safeParse(body);
if (!meetingEndedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { room, meeting_id } = meetingEndedResponse.data.payload;
@@ -159,7 +154,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const booking = await getBooking(bookingReference.bookingId as number);
if (!booking.eventType?.canSendCalVideoTranscriptionEmails) {
return res.status(200).json({
return NextResponse.json({
message: `Transcription emails are disabled for this event type ${booking.eventTypeId}`,
});
}
@@ -167,21 +162,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const transcripts = await getAllTranscriptsAccessLinkFromMeetingId(meeting_id);
if (!transcripts || !transcripts.length)
return res
.status(200)
.json({ message: `No Transcripts found for room name ${room} and meeting id ${meeting_id}` });
return NextResponse.json({
message: `No Transcripts found for room name ${room} and meeting id ${meeting_id}`,
});
const evt = await getCalendarEvent(booking);
await sendDailyVideoTranscriptEmails(evt, transcripts);
return res.status(200).json({ message: "Success" });
} else if (req.body?.type === "batch-processor.job-finished") {
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(req.body);
return NextResponse.json({ message: "Success" });
} else if (body?.type === "batch-processor.job-finished") {
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(body);
if (!batchProcessorJobFinishedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
}
const { id, input } = batchProcessorJobFinishedResponse.data.payload;
@@ -217,23 +210,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
});
return res.status(200).json({ message: "Success" });
return NextResponse.json({ message: "Success" });
} else {
log.error("Invalid type in /recorded-daily-video", req.body);
return res.status(200).json({ message: "Invalid type in /recorded-daily-video" });
log.error("Invalid type in /recorded-daily-video", body);
return NextResponse.json({ message: "Invalid type in /recorded-daily-video" });
}
} catch (err) {
log.error("Error in /recorded-daily-video", err);
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ message: err.message });
return NextResponse.json({ message: err.message }, { status: err.statusCode });
} else {
return res.status(500).json({ message: "something went wrong" });
return NextResponse.json({ message: "something went wrong" }, { status: 500 });
}
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
@@ -0,0 +1,141 @@
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { DIRECTORY_IDS_TO_LOG } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: ["[scim]"] });
// Extract the auth token from the request headers
const extractAuthToken = (req: NextRequest): string | null => {
const authHeader = req.headers.get("authorization") || null;
return authHeader ? authHeader.split(" ")[1] : null;
};
// Handle the SCIM events
const handleEvents = async (event: DirectorySyncEvent) => {
log.debug("handleEvents", safeStringify(event));
const dSyncData = await prisma.dSyncData.findFirst({
where: {
directoryId: event.directory_id,
},
select: {
id: true,
organizationId: true,
},
});
if (!dSyncData) {
throw new Error("Directory sync data not found");
}
const { organizationId } = dSyncData;
if (!organizationId) {
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
}
if (event.event.includes("group")) {
handleGroupEvents(event, organizationId);
}
if (event.event === "user.created" || event.event === "user.updated") {
await handleUserEvents(event, organizationId);
}
};
// This is the handler for the SCIM API requests
export async function GET(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "GET", params.directory);
}
export async function POST(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "POST", params.directory);
}
export async function PUT(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PUT", params.directory);
}
export async function PATCH(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "PATCH", params.directory);
}
export async function DELETE(request: NextRequest, { params }: { params: { directory?: string[] } }) {
return handleScimRequest(request, "DELETE", params.directory);
}
async function handleScimRequest(request: NextRequest, method: string, directoryParams?: string[]) {
const { dsyncController } = await jackson();
if (!directoryParams || directoryParams.length === 0) {
return NextResponse.json({ error: "Missing directory parameters" }, { status: 400 });
}
const [directoryId, path, resourceId] = directoryParams;
const shouldLog = DIRECTORY_IDS_TO_LOG.includes(directoryId);
const searchParams = request.nextUrl.searchParams;
if (shouldLog) {
console.log(
"SCIM API request",
safeStringify({
method,
url: request.url,
params: directoryParams,
searchParams: Object.fromEntries(searchParams.entries()),
body: request.body
? await request
.clone()
.json()
.catch(() => ({}))
: undefined,
})
);
}
let body: object | undefined = undefined;
try {
body = await request.json().catch(() => undefined);
} catch (e) {
log.error(`Error parsing SCIM event for directoryId ${directoryId} with error: ${e}`);
}
// Handle the SCIM API requests
const scimRequest: DirectorySyncRequest = {
method,
directoryId,
resourceId,
apiSecret: extractAuthToken(request),
resourceType: path === "Users" ? "users" : "groups",
body,
query: {
count: searchParams.get("count") ? parseInt(searchParams.get("count") as string) : undefined,
startIndex: searchParams.get("startIndex")
? parseInt(searchParams.get("startIndex") as string)
: undefined,
filter: searchParams.get("filter") as string,
},
};
const { status, data } = await dsyncController.requests.handle(scimRequest, handleEvents);
if (shouldLog) {
console.log(
"Response to SCIM",
safeStringify({
status,
data,
})
);
}
return NextResponse.json(data, { status });
}
+1 -1
View File
@@ -39,7 +39,7 @@ const genericSchema = z.object({
});
async function handler(req: NextRequest) {
const { searchParams } = new URL(`${req.url}`);
const { searchParams } = req.nextUrl;
const imageType = searchParams.get("type");
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
+96
View File
@@ -0,0 +1,96 @@
import { createHmac } from "crypto";
import { headers } from "next/headers";
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import getRawBody from "raw-body";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { default as webPrisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const helpscoutRequestBodySchema = z.object({
customer: z.object({
email: emailSchema,
}),
});
/**
* API for Helpscout to retrieve key information about a user from a ticket
* Note: HelpScout expects a JSON with a `html` prop to show its content as HTML
*/
export async function POST(request: NextRequest) {
const hsSignature = request.headers.get("x-helpscout-signature");
if (!hsSignature) return NextResponse.json({ message: "Missing signature" }, { status: 400 });
if (!process.env.CALENDSO_ENCRYPTION_KEY)
return NextResponse.json({ message: "Missing encryption key" }, { status: 500 });
const legacyRequest = buildLegacyRequest(headers(), cookies());
// Get the raw request body
const rawBody = await getRawBody(legacyRequest);
try {
const parsedBody = helpscoutRequestBodySchema.safeParse(JSON.parse(rawBody.toString()));
if (!parsedBody.success) return NextResponse.json({ message: "Invalid request body" }, { status: 400 });
// Verify the signature
const calculatedSig = createHmac("sha1", process.env.CALENDSO_ENCRYPTION_KEY)
.update(rawBody)
.digest("base64");
if (hsSignature !== calculatedSig)
return NextResponse.json({ message: "Invalid signature" }, { status: 400 });
const user = await webPrisma.user.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
username: true,
id: true,
createdDate: true,
},
});
if (!user) return NextResponse.json({ html: "User not found" });
const lastBooking = await webPrisma.attendee.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
booking: {
select: {
createdAt: true,
},
},
},
orderBy: {
booking: {
createdAt: "desc",
},
},
});
return NextResponse.json({
html: `
<ul>
<li><b>Username:</b>&nbsp;${user.username}</li>
<li><b>Last booking:</b>&nbsp;${
lastBooking && lastBooking.booking
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: "No info"
}</li>
<li><b>Account created:</b>&nbsp;${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
</ul>
`,
});
} catch (error) {
console.error("Error processing HelpScout request:", error);
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
}
}
@@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { _MembershipModel as Membership, _TeamModel as Team } from "@calcom/prisma/zod";
@@ -24,49 +24,58 @@ type CheckoutSessionMetadata = z.infer<typeof checkoutSessionMetadataSchema>;
export const schemaTeamReadPublic = Team.omit({});
export const schemaMembershipPublic = Membership.merge(z.object({ team: Team }).partial());
async function handler(req: NextApiRequest, res: NextApiResponse) {
const checkoutSession = await getCheckoutSession(req);
validateCheckoutSession(checkoutSession);
const checkoutSessionSubscription = getCheckoutSessionSubscription(checkoutSession);
const checkoutSessionMetadata = getCheckoutSessionMetadata(checkoutSession);
async function handler(request: NextRequest) {
try {
const { session_id } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
const finalizedTeam = await prisma.team.update({
where: { id: checkoutSessionMetadata.pendingPaymentTeamId },
data: {
pendingPayment: false,
members: {
create: {
userId: checkoutSessionMetadata.ownerId as number,
role: MembershipRole.OWNER,
accepted: true,
const checkoutSession = await getCheckoutSession(session_id);
validateCheckoutSession(checkoutSession);
const checkoutSessionSubscription = getCheckoutSessionSubscription(checkoutSession);
const checkoutSessionMetadata = getCheckoutSessionMetadata(checkoutSession);
const finalizedTeam = await prisma.team.update({
where: { id: checkoutSessionMetadata.pendingPaymentTeamId },
data: {
pendingPayment: false,
members: {
create: {
userId: checkoutSessionMetadata.ownerId as number,
role: MembershipRole.OWNER,
accepted: true,
},
},
metadata: {
paymentId: checkoutSession.id,
subscriptionId: checkoutSessionSubscription.id || null,
subscriptionItemId: checkoutSessionSubscription.items.data[0].id || null,
},
},
metadata: {
paymentId: checkoutSession.id,
subscriptionId: checkoutSessionSubscription.id || null,
subscriptionItemId: checkoutSessionSubscription.items.data[0].id || null,
},
},
include: { members: true },
});
include: { members: true },
});
const response = JSON.stringify(
{
const response = {
message: `Team created successfully. We also made user with ID=${checkoutSessionMetadata.ownerId} the owner of this team.`,
team: schemaTeamReadPublic.parse(finalizedTeam),
owner: schemaMembershipPublic.parse(finalizedTeam.members[0]),
},
null,
2
);
};
return res.status(200).send(response);
return NextResponse.json(response);
} catch (error) {
console.error("Error creating team:", error);
if (error instanceof HttpError) {
return NextResponse.json({ message: error.message }, { status: error.statusCode });
}
return NextResponse.json(
{ message: error instanceof Error ? error.message : "An unexpected error occurred" },
{ status: 500 }
);
}
}
async function getCheckoutSession(req: NextApiRequest) {
const { session_id } = querySchema.parse(req.query);
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id, {
async function getCheckoutSession(sessionId: string) {
const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ["subscription"],
});
if (!checkoutSession) throw new HttpError({ statusCode: 404, message: "Checkout session not found" });
@@ -102,11 +111,7 @@ function getCheckoutSessionMetadata(
});
}
const checkoutSessionMetadata = parseCheckoutSessionMetadata.data;
return checkoutSessionMetadata;
return parseCheckoutSessionMetadata.data;
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
export const GET = defaultResponderForAppDir(handler);
@@ -1,5 +1,6 @@
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -9,14 +10,19 @@ const scopes = [
"https://www.googleapis.com/auth/admin.directory.customer.readonly",
];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
export async function GET(request: NextRequest) {
try {
// Get appKeys from google-calendar
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
if (!client_id || typeof client_id !== "string")
return res.status(400).json({ message: "Google client_id missing." });
return NextResponse.json({ message: "Google client_id missing." }, { status: 400 });
if (!client_secret || typeof client_secret !== "string")
return res.status(400).json({ message: "Google client_secret missing." });
return NextResponse.json({ message: "Google client_secret missing." }, { status: 400 });
// Get teamId from query params
const teamId = request.nextUrl.searchParams.get("teamId");
// use different callback to normal calendar connection
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
@@ -25,11 +31,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
prompt: "consent",
state: JSON.stringify({ teamId: req.query.teamId }),
state: JSON.stringify({ teamId }),
});
res.status(200).json({ url: authUrl });
return NextResponse.json({ url: authUrl });
} catch (error) {
console.error("Error generating Google Workspace auth URL:", error);
return NextResponse.json(
{ message: error instanceof Error ? error.message : "An unexpected error occurred" },
{ status: 500 }
);
}
}
@@ -0,0 +1,86 @@
import { OAuth2Client } from "googleapis-common";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const stateSchema = z.object({
teamId: z.string(),
});
export async function GET(request: NextRequest) {
try {
const headersList = headers();
const cookiesList = cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session?.user?.id) {
return NextResponse.json({ message: "You must be logged in to do this" }, { status: 401 });
}
const code = request.nextUrl.searchParams.get("code");
const state = request.nextUrl.searchParams.get("state");
if (!state) {
return NextResponse.json({ message: "No state provided" }, { status: 400 });
}
const parsedState = stateSchema.parse(JSON.parse(state));
const { teamId } = parsedState;
await throwIfNotHaveAdminAccessToTeam({
teamId: Number(teamId) ?? null,
userId: session.user.id,
});
if (!code || typeof code !== "string") {
return NextResponse.json({ message: "`code` must be a string" }, { status: 400 });
}
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
if (!client_id || typeof client_id !== "string")
return NextResponse.json({ message: "Google client_id missing." }, { status: 400 });
if (!client_secret || typeof client_secret !== "string")
return NextResponse.json({ message: "Google client_secret missing." }, { status: 400 });
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
const credentials = await oAuth2Client.getToken(code);
await prisma.credential.create({
data: {
type: "google_workspace_directory",
key: credentials.res?.data,
userId: session.user.id,
},
});
let redirectUrl = `${WEBAPP_URL}/settings`;
if (teamId) {
redirectUrl = `${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`;
}
const safeRedirectUrl = getSafeRedirectUrl(redirectUrl) ?? `${WEBAPP_URL}/teams`;
return NextResponse.redirect(safeRedirectUrl);
} catch (error) {
console.error("Error in Google Workspace callback:", error);
// Redirect to teams page on error
return NextResponse.redirect(`${WEBAPP_URL}/teams`);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const bodySchema = z.object({
username: z.string(),
orgSlug: z.string().optional(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { username, orgSlug } = bodySchema.parse(body);
const legacyReq = buildLegacyRequest(headers(), cookies());
// Get current org domain from request headers
const { currentOrgDomain } = orgDomainConfig(legacyReq);
const result = await checkUsername(username, currentOrgDomain || orgSlug);
return NextResponse.json(result);
} catch (error) {
console.error(error);
return NextResponse.json({ error: "Failed to check username availability" }, { status: 400 });
}
}
@@ -0,0 +1,149 @@
import { headers, cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import { createCallerFactory } from "@calcom/trpc/server/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
bookingUid: z.string(),
userId: z.string(),
});
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const queryParams = Object.fromEntries(request.nextUrl.searchParams.entries());
try {
const { action, token, bookingUid, userId } = querySchema.parse(queryParams);
if (action === DirectAction.REJECT) {
// Rejections should use POST method
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Rejection requires POST method")}`
);
}
return await handleBookingAction(action, token, bookingUid, userId, undefined, request);
} catch (error) {
const bookingUid = queryParams.bookingUid || "";
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
}
export async function POST(request: NextRequest) {
const url = new URL(request.url);
const queryParams = Object.fromEntries(request.nextUrl.searchParams.entries());
try {
const { action, token, bookingUid, userId } = querySchema.parse(queryParams);
const body = await request.json().catch(() => ({}));
const { reason } = z.object({ reason: z.string().optional() }).parse(body || {});
return await handleBookingAction(action, token, bookingUid, userId, reason, request);
} catch (error) {
const bookingUid = queryParams.bookingUid || "";
return NextResponse.redirect(
`${url.origin}/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
}
async function handleBookingAction(
action: DirectAction,
token: string,
bookingUid: string,
userId: string,
reason?: string,
request?: NextRequest
) {
const booking = await prisma.booking.findUnique({
where: { oneTimePassword: token },
});
if (!booking) {
return NextResponse.redirect(
`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`
);
}
const user = await prisma.user.findUniqueOrThrow({
where: { id: Number(userId) },
});
/** We shape the session as required by tRPC router */
async function sessionGetter() {
return {
user: {
id: Number(userId),
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
}
try {
/** @see https://trpc.io/docs/server-side-calls */
const createCaller = createCallerFactory(bookingsRouter);
// Use buildLegacyRequest to create a request object compatible with Pages Router
const legacyReq = request ? buildLegacyRequest(headers(), cookies()) : ({} as any);
const res = {} as any;
const ctx = await createContext({ req: legacyReq, res }, sessionGetter);
const caller = createCaller({
...ctx,
req: legacyReq,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
/** Ignored reason input unless we're rejecting */
reason: action === DirectAction.REJECT ? reason : undefined,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
return NextResponse.redirect(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`);
}
await prisma.booking.update({
where: { id: booking.id },
data: { oneTimePassword: null },
});
return NextResponse.redirect(`/booking/${booking.uid}`);
}
+6
View File
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import * as pjson from "package.json";
export async function GET() {
return NextResponse.json({ version: pjson.version });
}
@@ -0,0 +1,103 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
const appCredentialWebhookRequestBodySchema = z.object({
// UserId of the cal.com user
userId: z.number().int(),
appSlug: z.string(),
// Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
keys: z.string(),
});
export async function POST(request: NextRequest) {
if (!APP_CREDENTIAL_SHARING_ENABLED) {
return NextResponse.json({ message: "Credential sharing is not enabled" }, { status: 403 });
}
const secretHeader = request.headers.get(CREDENTIAL_SYNC_SECRET_HEADER_NAME);
if (secretHeader !== CREDENTIAL_SYNC_SECRET) {
return NextResponse.json({ message: "Invalid credential sync secret" }, { status: 403 });
}
try {
const body = await request.json();
const reqBodyParsed = appCredentialWebhookRequestBodySchema.safeParse(body);
if (!reqBodyParsed.success) {
return NextResponse.json({ error: reqBodyParsed.error.issues }, { status: 400 });
}
const reqBody = reqBodyParsed.data;
const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
const app = await prisma.app.findUnique({
where: { slug: reqBody.appSlug },
select: { dirName: true },
});
if (!app) {
return NextResponse.json({ message: "App not found" }, { status: 404 });
}
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
if (!appMetadata) {
return NextResponse.json(
{ message: "App not found. Ensure that you have the correct app slug" },
{ status: 404 }
);
}
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
// INFO: Can't use prisma upsert as we don't know the id of the credential
const appCredential = await prisma.credential.findFirst({
where: {
userId: reqBody.userId,
appId: appMetadata.slug,
},
select: {
id: true,
},
});
if (appCredential) {
await prisma.credential.update({
where: {
id: appCredential.id,
},
data: {
key: keys,
},
});
return NextResponse.json({ message: `Credentials updated for userId: ${reqBody.userId}` });
} else {
await prisma.credential.create({
data: {
key: keys,
userId: reqBody.userId,
appId: appMetadata.slug,
type: appMetadata.type,
},
});
return NextResponse.json({ message: `Credentials created for userId: ${reqBody.userId}` });
}
} catch (error) {
console.error("Error processing app credential webhook:", error);
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
}
}
@@ -9,10 +9,9 @@ import {
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectWebhookToHaveBeenCalledWith } from "@calcom/web/test/utils/bookingScenario/expects";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { NextRequest } from "next/server";
import { createMocks } from "node-mocks-http";
import { describe, afterEach, test, vi, beforeEach, beforeAll } from "vitest";
import { describe, afterEach, test, vi, beforeEach, beforeAll, expect } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
@@ -20,10 +19,44 @@ import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient"
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import handler from "@calcom/web/pages/api/recorded-daily-video";
import * as recordedDailyVideoRoute from "@calcom/web/app/api/recorded-daily-video/route";
// Mock the next/headers module before importing the handler
vi.mock("next/headers", () => ({
headers: () => new Headers({ "content-type": "application/json" }),
cookies: () => ({
get: () => null,
getAll: () => [],
has: () => false,
}),
}));
// Mock NextResponse to handle the response properly
vi.mock("next/server", async () => {
const actual = (await vi.importActual("next/server")) as any;
return {
...actual,
NextResponse: {
json: (data: any, init?: ResponseInit) => {
return new Response(JSON.stringify(data), {
...init,
headers: {
...init?.headers,
"content-type": "application/json",
},
});
},
// Add other methods you might need
redirect: actual.NextResponse.redirect,
next: actual.NextResponse.next,
rewrite: actual.NextResponse.rewrite,
},
};
});
// Now import the handler after mocking
const { POST: handler } = recordedDailyVideoRoute;
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
beforeAll(() => {
// Setup env vars
vi.stubEnv("SENDGRID_API_KEY", "FAKE_SENDGRID_API_KEY");
@@ -112,6 +145,24 @@ const TRANSCRIPTION_ACCESS_LINK = {
],
};
// We may need to make this more globally available. Will move if we need it elsewhere
function createNextRequest(mockReq: ReturnType<typeof createMocks>["req"]): NextRequest {
// Create a Request object that NextRequest can wrap
const request = new Request("https://example.com/api/recorded-daily-video", {
method: mockReq.method,
headers: new Headers(mockReq.headers as Record<string, string>),
body: mockReq.body ? JSON.stringify(mockReq.body) : undefined,
});
// Create a NextRequest from the Request
const nextRequest = new NextRequest(request, {
ip: mockReq.socket?.remoteAddress || "127.0.0.1",
geo: { city: "", country: "", region: "" },
});
return nextRequest;
}
describe("Handler: /api/recorded-daily-video", () => {
beforeEach(() => {
fetchMock.resetMocks();
@@ -212,13 +263,23 @@ describe("Handler: /api/recorded-daily-video", () => {
download_link: recordingDownloadLink,
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks({
method: "POST",
body: BATCH_PROCESSOR_JOB_FINSISHED_PAYLOAD,
prisma,
});
await handler(req, res);
const nextReq = createNextRequest(req);
// Handle the response differently
try {
const response = await handler(nextReq);
// For App Router, we just need to check if the response exists
expect(response).toBeDefined();
} catch (error) {
console.error("Handler error:", error);
throw error;
}
await expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
@@ -1,22 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
expiresAt: {
lte: new Date(Date.now()),
},
},
});
res.json({ ok: true, count: deleted.count });
}
-66
View File
@@ -1,66 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { renderEmail } from "@calcom/emails";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
await renderEmail("MonthlyDigestEmail", {
language: t,
Created: 12,
Completed: 13,
Rescheduled: 14,
Cancelled: 16,
mostBookedEvents: [
{
eventTypeId: 3,
eventTypeName: "Test1",
count: 3,
},
{
eventTypeId: 4,
eventTypeName: "Test2",
count: 5,
},
],
membersWithMostBookings: [
{
userId: 4,
user: {
id: 4,
name: "User1 name",
email: "email.com",
avatar: "none",
username: "User1",
},
count: 4,
},
{
userId: 6,
user: {
id: 6,
name: "User2 name",
email: "email2.com",
avatar: "none",
username: "User2",
},
count: 8,
},
],
admin: { email: "admin.com", name: "admin" },
team: { name: "Team1", id: 4 },
})
);
res.end();
};
export default handler;
-34
View File
@@ -1,34 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FUTURE_ROUTES_OVERRIDE_COOKIE_NAME as COOKIE_NAME } from "@calcom/lib/constants";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const session = await getServerSession({ req });
if (!session || !session.user || !session.user.email) {
res.status(401).json({ message: "Not authenticated" });
return;
}
let redirectUrl = "/";
// We take you back where you came from if possible
if (typeof req.headers["referer"] === "string") redirectUrl = req.headers["referer"];
// If has the cookie, Opt-out of V2
if (COOKIE_NAME in req.cookies && req.cookies[COOKIE_NAME] === "1") {
res.setHeader("Set-Cookie", `${COOKIE_NAME}=0; Max-Age=0; Path=/`);
} else {
/* Opt-int to V2 */
res.setHeader("Set-Cookie", `${COOKIE_NAME}=1; Path=/`);
}
res.redirect(redirectUrl);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
-7
View File
@@ -1,7 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const country = req.headers["x-vercel-ip-country"] || "Unknown";
res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400");
res.status(200).json({ country });
}
-28
View File
@@ -1,28 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import crypto from "node:crypto";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req });
const secret = process.env.INTERCOM_SECRET;
if (!session) {
return res.status(401).json({ message: "user not authenticated" });
}
if (!secret) {
return res.status(400).json({ message: "Intercom Identity Verification secret not set" });
}
const hmac = crypto.createHmac("sha256", secret);
hmac.update(String(session.user.id));
const hash = hmac.digest("hex");
return res.status(200).json({ hash });
}
export default defaultHandler({
GET: Promise.resolve({ default: handler }),
});
-91
View File
@@ -1,91 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import type { UserProfile } from "@calcom/types/UserProfile";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
reason: z.string().optional(),
});
const decryptedSchema = z.object({
bookingUid: z.string(),
userId: z.number().int(),
});
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, reason } = querySchema.parse(req.query);
const { bookingUid, userId } = decryptedSchema.parse(
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
);
const booking = await prisma.booking.findUniqueOrThrow({
where: { uid: bookingUid },
});
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
});
/** We shape the session as required by tRPC router */
async function sessionGetter() {
return {
user: {
id: userId,
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
}
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = bookingsRouter.createCaller({
...ctx,
req,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
return;
}
res.redirect(`/booking/${bookingUid}`);
}
export default defaultResponder(handler);
-9
View File
@@ -1,9 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
type Response = {
message: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
return res.status(400).json({ message: "Please don't" });
}
@@ -1,109 +0,0 @@
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
import type { NextApiRequest, NextApiResponse } from "next";
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { DIRECTORY_IDS_TO_LOG } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: ["[scim]"] });
// This is the handler for the SCIM API requests
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { dsyncController } = await jackson();
const { method, query, body } = req;
const [directoryId, path, resourceId] = query.directory as string[];
const shouldLog = DIRECTORY_IDS_TO_LOG.includes(directoryId);
if (shouldLog) {
console.log(
"SCIM API request",
safeStringify({
method: req.method,
url: req.url,
query: req.query,
body: req.body,
})
);
}
let responseBody: object | undefined = undefined;
if (body) {
try {
responseBody = JSON.parse(body);
} catch (e) {
log.error(`Error parsing SCIM event for directoryId ${directoryId} with error: ${e} and body ${body}`);
}
}
// Handle the SCIM API requests
const request: DirectorySyncRequest = {
method: method as string,
directoryId,
resourceId,
apiSecret: extractAuthToken(req),
resourceType: path === "Users" ? "users" : "groups",
body: responseBody,
query: {
count: req.query.count ? parseInt(req.query.count as string) : undefined,
startIndex: req.query.startIndex ? parseInt(req.query.startIndex as string) : undefined,
filter: req.query.filter as string,
},
};
const { status, data } = await dsyncController.requests.handle(request, handleEvents);
if (shouldLog) {
console.log(
"Response to SCIM",
safeStringify({
status,
data,
})
);
}
res.status(status).json(data);
}
// Fetch the auth token from the request headers
export const extractAuthToken = (req: NextApiRequest): string | null => {
const authHeader = req.headers.authorization || null;
return authHeader ? authHeader.split(" ")[1] : null;
};
// Handle the SCIM events
const handleEvents = async (event: DirectorySyncEvent) => {
log.debug("handleEvents", safeStringify(event));
const dSyncData = await prisma.dSyncData.findFirst({
where: {
directoryId: event.directory_id,
},
select: {
id: true,
organizationId: true,
},
});
if (!dSyncData) {
throw new Error("Directory sync data not found");
}
const { organizationId } = dSyncData;
if (!organizationId) {
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
}
if (event.event.includes("group")) {
handleGroupEvents(event, organizationId);
}
if (event.event === "user.created" || event.event === "user.updated") {
await handleUserEvents(event, organizationId);
}
};
@@ -1,88 +0,0 @@
import { createHmac } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import getRawBody from "raw-body";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { default as webPrisma } from "@calcom/prisma";
export const config = {
api: {
bodyParser: false,
},
};
const helpscoutRequestBodySchema = z.object({
customer: z.object({
email: emailSchema,
}),
});
/**
* API for Helpscout to retrieve key information about a user from a ticket
* Note: HelpScout expects a JSON with a `html` prop to show its content as HTML
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" });
const hsSignature = req.headers["x-helpscout-signature"];
if (!hsSignature) return res.status(400).end();
if (!process.env.CALENDSO_ENCRYPTION_KEY) return res.status(500).end();
const rawBody = await getRawBody(req);
const parsedBody = helpscoutRequestBodySchema.safeParse(JSON.parse(rawBody.toString()));
if (!parsedBody.success) return res.status(400).end();
const calculatedSig = createHmac("sha1", process.env.CALENDSO_ENCRYPTION_KEY)
.update(rawBody)
.digest("base64");
if (req.headers["x-helpscout-signature"] !== calculatedSig) return res.status(400).end();
const user = await webPrisma.user.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
username: true,
id: true,
createdDate: true,
},
});
if (!user) return res.status(200).json({ html: "User not found" });
const lastBooking = await webPrisma.attendee.findFirst({
where: {
email: parsedBody.data.customer.email,
},
select: {
booking: {
select: {
createdAt: true,
},
},
},
orderBy: {
booking: {
createdAt: "desc",
},
},
});
return res.status(200).json({
html: `
<ul>
<li><b>Username:</b>&nbsp;${user.username}</li>
<li><b>Last booking:</b>&nbsp;${
lastBooking && lastBooking.booking
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: "No info"
}</li>
<li><b>Account created:</b>&nbsp;${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
</ul>
`,
});
}
@@ -1,64 +0,0 @@
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
const stateSchema = z.object({
teamId: z.string(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const { code, state } = req.query;
const parsedState = stateSchema.parse(JSON.parse(state as string));
const { teamId } = parsedState;
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: session.user.id });
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
if (!client_id || typeof client_id !== "string")
return res.status(400).json({ message: "Google client_id missing." });
if (!client_secret || typeof client_secret !== "string")
return res.status(400).json({ message: "Google client_secret missing." });
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
if (!code) {
throw new Error("No code provided");
}
const credentials = await oAuth2Client.getToken(code);
await prisma.credential.create({
data: {
type: "google_workspace_directory",
key: credentials.res?.data,
userId: session.user.id,
},
});
if (!teamId) {
res.redirect(getSafeRedirectUrl(`${WEBAPP_URL}/settings`) ?? `${WEBAPP_URL}/teams`);
}
res.redirect(
getSafeRedirectUrl(`${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ??
`${WEBAPP_URL}/teams`
);
}
-22
View File
@@ -1,22 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { checkUsername } from "@calcom/lib/server/checkUsername";
type Response = {
available: boolean;
premium: boolean;
};
const bodySchema = z.object({
username: z.string(),
orgSlug: z.string().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
const { currentOrgDomain } = orgDomainConfig(req);
const { username, orgSlug } = bodySchema.parse(req.body);
const result = await checkUsername(username, currentOrgDomain || orgSlug);
return res.status(200).json(result);
}
@@ -1,98 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import { createCallerFactory } from "@calcom/trpc/server/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";
import { TRPCError } from "@trpc/server";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
bookingUid: z.string(),
userId: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, bookingUid, userId } = querySchema.parse(req.query);
// Rejections runs on a POST request, confirming on a GET request.
const { reason } = z.object({ reason: z.string().optional() }).parse(req.body || {});
const booking = await prisma.booking.findUnique({
where: { oneTimePassword: token },
});
if (!booking) {
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`);
return;
}
const user = await prisma.user.findUniqueOrThrow({
where: { id: Number(userId) },
});
/** We shape the session as required by tRPC router */
async function sessionGetter() {
return {
user: {
id: Number(userId),
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
}
try {
/** @see https://trpc.io/docs/server-side-calls */
const createCaller = createCallerFactory(bookingsRouter);
const ctx = await createContext({ req, res }, sessionGetter);
const caller = createCaller({
...ctx,
req,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
/** Ignored reason input unless we're rejecting */
reason: action === DirectAction.REJECT ? reason : undefined,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
res.redirect(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`);
return;
}
await prisma.booking.update({
where: { id: booking.id },
data: { oneTimePassword: null },
});
res.redirect(`/booking/${booking.uid}`);
}
export default defaultResponder(handler);
-10
View File
@@ -1,10 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import * as pjson from "package.json";
type Response = {
version: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
return res.status(200).json({ version: pjson.version });
}
@@ -1,91 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
const appCredentialWebhookRequestBodySchema = z.object({
// UserId of the cal.com user
userId: z.number().int(),
appSlug: z.string(),
// Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
keys: z.string(),
});
/** */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!APP_CREDENTIAL_SHARING_ENABLED) {
return res.status(403).json({ message: "Credential sharing is not enabled" });
}
if (req.headers[CREDENTIAL_SYNC_SECRET_HEADER_NAME] !== CREDENTIAL_SYNC_SECRET) {
return res.status(403).json({ message: "Invalid credential sync secret" });
}
const reqBodyParsed = appCredentialWebhookRequestBodySchema.safeParse(req.body);
if (!reqBodyParsed.success) {
return res.status(400).json({ error: reqBodyParsed.error.issues });
}
const reqBody = reqBodyParsed.data;
const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });
if (!user) {
return res.status(404).json({ message: "User not found" });
}
const app = await prisma.app.findUnique({
where: { slug: reqBody.appSlug },
select: { dirName: true },
});
if (!app) {
return res.status(404).json({ message: "App not found" });
}
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
if (!appMetadata) {
return res.status(404).json({ message: "App not found. Ensure that you have the correct app slug" });
}
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
// INFO: Can't use prisma upsert as we don't know the id of the credential
const appCredential = await prisma.credential.findFirst({
where: {
userId: reqBody.userId,
appId: appMetadata.slug,
},
select: {
id: true,
},
});
if (appCredential) {
await prisma.credential.update({
where: {
id: appCredential.id,
},
data: {
key: keys,
},
});
return res.status(200).json({ message: `Credentials updated for userId: ${reqBody.userId}` });
} else {
await prisma.credential.create({
data: {
key: keys,
userId: reqBody.userId,
appId: appMetadata.slug,
type: appMetadata.type,
},
});
return res.status(200).json({ message: `Credentials created for userId: ${reqBody.userId}` });
}
}