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:
+9
-10
@@ -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 });
|
||||
}
|
||||
+7
-11
@@ -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 });
|
||||
}
|
||||
+9
-11
@@ -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 });
|
||||
}
|
||||
+9
-12
@@ -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 });
|
||||
}
|
||||
+7
-10
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+12
-9
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
+39
-51
@@ -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 });
|
||||
}
|
||||
@@ -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([
|
||||
|
||||
@@ -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> ${user.username}</li>
|
||||
<li><b>Last booking:</b> ${
|
||||
lastBooking && lastBooking.booking
|
||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||
: "No info"
|
||||
}</li>
|
||||
<li><b>Account created:</b> ${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 });
|
||||
}
|
||||
}
|
||||
+47
-42
@@ -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);
|
||||
+19
-8
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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) }),
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 }),
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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> ${user.username}</li>
|
||||
<li><b>Last booking:</b> ${
|
||||
lastBooking && lastBooking.booking
|
||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||
: "No info"
|
||||
}</li>
|
||||
<li><b>Account created:</b> ${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`
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}` });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user