first commit
This commit is contained in:
1
calcom/apps/api/v1/lib/constants.ts
Normal file
1
calcom/apps/api/v1/lib/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PRISMA_CLIENT_CACHING_TIME = 1000 * 60 * 60 * 24; // one day in ms
|
||||
24
calcom/apps/api/v1/lib/helpers/addRequestid.ts
Normal file
24
calcom/apps/api/v1/lib/helpers/addRequestid.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
export const addRequestId: NextMiddleware = async (_req, res, next) => {
|
||||
// Apply header with unique ID to every request
|
||||
res.setHeader("Calcom-Response-ID", nanoid());
|
||||
// Add all headers here instead of next.config.js as it is throwing error( Cannot set headers after they are sent to the client) for OPTIONS method
|
||||
// It is known to happen only in Dev Mode.
|
||||
res.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, PATCH, DELETE, POST, PUT");
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Headers",
|
||||
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization"
|
||||
);
|
||||
|
||||
// Ensure all OPTIONS request are automatically successful. Headers are already set above.
|
||||
if (_req.method === "OPTIONS") {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
// Let remaining middleware and API route execute
|
||||
await next();
|
||||
};
|
||||
20
calcom/apps/api/v1/lib/helpers/captureErrors.ts
Normal file
20
calcom/apps/api/v1/lib/helpers/captureErrors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { captureException as SentryCaptureException } from "@sentry/nextjs";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
export const captureErrors: NextMiddleware = async (_req, res, next) => {
|
||||
try {
|
||||
// Catch any errors that are thrown in remaining
|
||||
// middleware and the API route handler
|
||||
await next();
|
||||
} catch (error) {
|
||||
SentryCaptureException(error);
|
||||
const redactedError = redactError(error);
|
||||
if (redactedError instanceof Error) {
|
||||
res.status(400).json({ message: redactedError.message, error: redactedError });
|
||||
return;
|
||||
}
|
||||
res.status(400).json({ message: "Something went wrong", error });
|
||||
}
|
||||
};
|
||||
23
calcom/apps/api/v1/lib/helpers/checkIsInMaintenanceMode.ts
Normal file
23
calcom/apps/api/v1/lib/helpers/checkIsInMaintenanceMode.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
return get<T>(key);
|
||||
} catch (error) {
|
||||
// Don't crash if EDGE_CONFIG env var is missing
|
||||
}
|
||||
};
|
||||
|
||||
export const config = { matcher: "/:path*" };
|
||||
|
||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||
if (isInMaintenanceMode) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
9
calcom/apps/api/v1/lib/helpers/extendRequest.ts
Normal file
9
calcom/apps/api/v1/lib/helpers/extendRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
export const extendRequest: NextMiddleware = async (req, res, next) => {
|
||||
req.pagination = {
|
||||
take: 100,
|
||||
skip: 0,
|
||||
};
|
||||
await next();
|
||||
};
|
||||
32
calcom/apps/api/v1/lib/helpers/httpMethods.ts
Normal file
32
calcom/apps/api/v1/lib/helpers/httpMethods.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
export const httpMethod = (allowedHttpMethod: "GET" | "POST" | "PATCH" | "DELETE"): NextMiddleware => {
|
||||
return async function (req, res, next) {
|
||||
if (req.method === allowedHttpMethod || req.method == "OPTIONS") {
|
||||
await next();
|
||||
} else {
|
||||
res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` });
|
||||
res.end();
|
||||
}
|
||||
};
|
||||
};
|
||||
// Made this so we can support several HTTP Methods in one route and use it there.
|
||||
// Could be further extracted into a third function or refactored into one.
|
||||
// that checks if it's just a string or an array and apply the correct logic to both cases.
|
||||
export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => {
|
||||
return async function (req, res, next) {
|
||||
if (allowedHttpMethod.some((method) => method === req.method || req.method == "OPTIONS")) {
|
||||
await next();
|
||||
} else {
|
||||
res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` });
|
||||
res.end();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const HTTP_POST = httpMethod("POST");
|
||||
export const HTTP_GET = httpMethod("GET");
|
||||
export const HTTP_PATCH = httpMethod("PATCH");
|
||||
export const HTTP_DELETE = httpMethod("DELETE");
|
||||
export const HTTP_GET_DELETE_PATCH = httpMethods(["GET", "DELETE", "PATCH"]);
|
||||
export const HTTP_GET_OR_POST = httpMethods(["GET", "POST"]);
|
||||
90
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts
Normal file
90
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
|
||||
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
||||
checkRateLimitAndThrowError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("rateLimitApiKey middleware", () => {
|
||||
it("should return 401 if no apiKey is provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {},
|
||||
});
|
||||
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
|
||||
});
|
||||
|
||||
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
reset: Date.now(),
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||
identifier: "test-key",
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("should set rate limit headers correctly", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse = {
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
|
||||
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
|
||||
});
|
||||
|
||||
it("should return 429 if rate limit is exceeded", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
||||
});
|
||||
});
|
||||
24
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.ts
Normal file
24
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
|
||||
// TODO: Add a way to add trusted api keys
|
||||
try {
|
||||
await checkRateLimitAndThrowError({
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(429).json({ message: "Rate limit exceeded" });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
14
calcom/apps/api/v1/lib/helpers/safeParseJSON.ts
Normal file
14
calcom/apps/api/v1/lib/helpers/safeParseJSON.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function parseJSONSafely(str: string) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
console.error((e as Error).message);
|
||||
if ((e as Error).message.includes("Unexpected token")) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Invalid JSON in the body: ${(e as Error).message}`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
46
calcom/apps/api/v1/lib/helpers/verifyApiKey.ts
Normal file
46
calcom/apps/api/v1/lib/helpers/verifyApiKey.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { isAdminGuard } from "../utils/isAdmin";
|
||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||
|
||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||
export const dateNotInPast = function (date: Date) {
|
||||
const now = new Date();
|
||||
if (now.setHours(0, 0, 0, 0) > date.setHours(0, 0, 0, 0)) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// This verifies the apiKey and sets the user if it is valid.
|
||||
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
if (!hasValidLicense && IS_PRODUCTION)
|
||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||
// Check if the apiKey query param is provided.
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
// remove the prefix from the user provided api_key. If no env set default to "cal_"
|
||||
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
|
||||
// Hash the key again before matching against the database records.
|
||||
const hashedKey = hashAPIKey(strippedApiKey);
|
||||
// Check if the hashed api key exists in database.
|
||||
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
|
||||
// If cannot find any api key. Throw a 401 Unauthorized.
|
||||
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
|
||||
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
|
||||
return res.status(401).json({ error: "This apiKey is expired" });
|
||||
}
|
||||
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
|
||||
// save the user id in the request for later use
|
||||
req.userId = apiKey.userId;
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||
|
||||
await next();
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => {
|
||||
const { isSystemWideAdmin } = req;
|
||||
|
||||
if (!isSystemWideAdmin) {
|
||||
return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" });
|
||||
}
|
||||
|
||||
if (!APP_CREDENTIAL_SHARING_ENABLED) {
|
||||
return res.status(501).json({ error: "Credential syncing is not enabled" });
|
||||
}
|
||||
|
||||
if (
|
||||
req.headers[process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME || "calcom-credential-sync-secret"] !==
|
||||
process.env.CALCOM_CREDENTIAL_SYNC_SECRET
|
||||
) {
|
||||
return res.status(401).json({ message: "Invalid credential sync secret" });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
51
calcom/apps/api/v1/lib/helpers/withMiddleware.ts
Normal file
51
calcom/apps/api/v1/lib/helpers/withMiddleware.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { label } from "next-api-middleware";
|
||||
|
||||
import { addRequestId } from "./addRequestid";
|
||||
import { captureErrors } from "./captureErrors";
|
||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||
import { extendRequest } from "./extendRequest";
|
||||
import {
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
HTTP_PATCH,
|
||||
HTTP_GET,
|
||||
HTTP_GET_OR_POST,
|
||||
HTTP_GET_DELETE_PATCH,
|
||||
} from "./httpMethods";
|
||||
import { rateLimitApiKey } from "./rateLimitApiKey";
|
||||
import { verifyApiKey } from "./verifyApiKey";
|
||||
import { verifyCredentialSyncEnabled } from "./verifyCredentialSyncEnabled";
|
||||
import { withPagination } from "./withPagination";
|
||||
|
||||
const middleware = {
|
||||
HTTP_GET_OR_POST,
|
||||
HTTP_GET_DELETE_PATCH,
|
||||
HTTP_GET,
|
||||
HTTP_PATCH,
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
addRequestId,
|
||||
checkIsInMaintenanceMode,
|
||||
verifyApiKey,
|
||||
rateLimitApiKey,
|
||||
extendRequest,
|
||||
pagination: withPagination,
|
||||
captureErrors,
|
||||
verifyCredentialSyncEnabled,
|
||||
};
|
||||
|
||||
type Middleware = keyof typeof middleware;
|
||||
|
||||
const middlewareOrder = [
|
||||
// The order here, determines the order of execution
|
||||
"checkIsInMaintenanceMode",
|
||||
"extendRequest",
|
||||
"captureErrors",
|
||||
"verifyApiKey",
|
||||
"rateLimitApiKey",
|
||||
"addRequestId",
|
||||
] as Middleware[]; // <-- Provide a list of middleware to call automatically
|
||||
|
||||
const withMiddleware = label(middleware, middlewareOrder);
|
||||
|
||||
export { withMiddleware, middleware, middlewareOrder };
|
||||
17
calcom/apps/api/v1/lib/helpers/withPagination.ts
Normal file
17
calcom/apps/api/v1/lib/helpers/withPagination.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
import z from "zod";
|
||||
|
||||
const withPage = z.object({
|
||||
page: z.coerce.number().min(1).optional().default(1),
|
||||
take: z.coerce.number().min(1).optional().default(10),
|
||||
});
|
||||
|
||||
export const withPagination: NextMiddleware = async (req, _, next) => {
|
||||
const { page, take } = withPage.parse(req.query);
|
||||
const skip = (page - 1) * take;
|
||||
req.pagination = {
|
||||
take,
|
||||
skip,
|
||||
};
|
||||
await next();
|
||||
};
|
||||
187
calcom/apps/api/v1/lib/types.ts
Normal file
187
calcom/apps/api/v1/lib/types.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import type {
|
||||
Attendee,
|
||||
Availability,
|
||||
Booking,
|
||||
BookingReference,
|
||||
Credential,
|
||||
DestinationCalendar,
|
||||
EventType,
|
||||
EventTypeCustomInput,
|
||||
Membership,
|
||||
Payment,
|
||||
ReminderMail,
|
||||
Schedule,
|
||||
SelectedCalendar,
|
||||
Team,
|
||||
User,
|
||||
Webhook,
|
||||
} from "@calcom/prisma/client";
|
||||
|
||||
// Base response, used for all responses
|
||||
export type BaseResponse = {
|
||||
message?: string;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
// User
|
||||
export type UserResponse = BaseResponse & {
|
||||
user?: Partial<User>;
|
||||
};
|
||||
|
||||
export type UsersResponse = BaseResponse & {
|
||||
users?: Partial<User>[];
|
||||
};
|
||||
|
||||
// Team
|
||||
export type TeamResponse = BaseResponse & {
|
||||
team?: Partial<Team>;
|
||||
owner?: Partial<Membership>;
|
||||
};
|
||||
export type TeamsResponse = BaseResponse & {
|
||||
teams?: Partial<Team>[];
|
||||
};
|
||||
|
||||
// SelectedCalendar
|
||||
export type SelectedCalendarResponse = BaseResponse & {
|
||||
selected_calendar?: Partial<SelectedCalendar>;
|
||||
};
|
||||
export type SelectedCalendarsResponse = BaseResponse & {
|
||||
selected_calendars?: Partial<SelectedCalendar>[];
|
||||
};
|
||||
|
||||
// Attendee
|
||||
export type AttendeeResponse = BaseResponse & {
|
||||
attendee?: Partial<Attendee>;
|
||||
};
|
||||
// Grouping attendees in booking arrays for now,
|
||||
// later might remove endpoint and move to booking endpoint altogether.
|
||||
export type AttendeesResponse = BaseResponse & {
|
||||
attendees?: Partial<Attendee>[];
|
||||
};
|
||||
|
||||
// Availability
|
||||
export type AvailabilityResponse = BaseResponse & {
|
||||
availability?: Partial<Availability>;
|
||||
};
|
||||
export type AvailabilitiesResponse = BaseResponse & {
|
||||
availabilities?: Partial<Availability>[];
|
||||
};
|
||||
|
||||
// BookingReference
|
||||
export type BookingReferenceResponse = BaseResponse & {
|
||||
booking_reference?: Partial<BookingReference>;
|
||||
};
|
||||
export type BookingReferencesResponse = BaseResponse & {
|
||||
booking_references?: Partial<BookingReference>[];
|
||||
};
|
||||
|
||||
// Booking
|
||||
export type BookingResponse = BaseResponse & {
|
||||
booking?: Partial<Booking>;
|
||||
};
|
||||
export type BookingsResponse = BaseResponse & {
|
||||
bookings?: Partial<Booking>[];
|
||||
};
|
||||
|
||||
// Credential
|
||||
export type CredentialResponse = BaseResponse & {
|
||||
credential?: Partial<Credential>;
|
||||
};
|
||||
export type CredentialsResponse = BaseResponse & {
|
||||
credentials?: Partial<Credential>[];
|
||||
};
|
||||
|
||||
// DestinationCalendar
|
||||
export type DestinationCalendarResponse = BaseResponse & {
|
||||
destination_calendar?: Partial<DestinationCalendar>;
|
||||
};
|
||||
export type DestinationCalendarsResponse = BaseResponse & {
|
||||
destination_calendars?: Partial<DestinationCalendar>[];
|
||||
};
|
||||
|
||||
// Membership
|
||||
export type MembershipResponse = BaseResponse & {
|
||||
membership?: Partial<Membership>;
|
||||
};
|
||||
export type MembershipsResponse = BaseResponse & {
|
||||
memberships?: Partial<Membership>[];
|
||||
};
|
||||
|
||||
// EventTypeCustomInput
|
||||
export type EventTypeCustomInputResponse = BaseResponse & {
|
||||
event_type_custom_input?: Partial<EventTypeCustomInput>;
|
||||
};
|
||||
export type EventTypeCustomInputsResponse = BaseResponse & {
|
||||
event_type_custom_inputs?: Partial<EventTypeCustomInput>[];
|
||||
};
|
||||
// From rrule https://jakubroztocil.github.io/rrule freq
|
||||
export enum Frequency {
|
||||
"YEARLY",
|
||||
"MONTHLY",
|
||||
"WEEKLY",
|
||||
"DAILY",
|
||||
"HOURLY",
|
||||
"MINUTELY",
|
||||
"SECONDLY",
|
||||
}
|
||||
interface EventTypeExtended extends Omit<EventType, "recurringEvent" | "locations"> {
|
||||
recurringEvent: {
|
||||
dtstart?: Date | undefined;
|
||||
interval?: number | undefined;
|
||||
count?: number | undefined;
|
||||
freq?: Frequency | undefined;
|
||||
until?: Date | undefined;
|
||||
tzid?: string | undefined;
|
||||
} | null;
|
||||
locations:
|
||||
| {
|
||||
link?: string | undefined;
|
||||
address?: string | undefined;
|
||||
hostPhoneNumber?: string | undefined;
|
||||
type: EventLocationType;
|
||||
}[]
|
||||
| null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| any;
|
||||
}
|
||||
|
||||
// EventType
|
||||
export type EventTypeResponse = BaseResponse & {
|
||||
event_type?: Partial<EventType | EventTypeExtended>;
|
||||
};
|
||||
export type EventTypesResponse = BaseResponse & {
|
||||
event_types?: Partial<EventType | EventTypeExtended>[];
|
||||
};
|
||||
|
||||
// Payment
|
||||
export type PaymentResponse = BaseResponse & {
|
||||
payment?: Partial<Payment>;
|
||||
};
|
||||
export type PaymentsResponse = BaseResponse & {
|
||||
payments?: Partial<Payment>[];
|
||||
};
|
||||
|
||||
// Schedule
|
||||
export type ScheduleResponse = BaseResponse & {
|
||||
schedule?: Partial<Schedule>;
|
||||
};
|
||||
export type SchedulesResponse = BaseResponse & {
|
||||
schedules?: Partial<Schedule>[];
|
||||
};
|
||||
|
||||
// Webhook
|
||||
export type WebhookResponse = BaseResponse & {
|
||||
webhook?: Partial<Webhook> | null;
|
||||
};
|
||||
export type WebhooksResponse = BaseResponse & {
|
||||
webhooks?: Partial<Webhook>[];
|
||||
};
|
||||
|
||||
// ReminderMail
|
||||
export type ReminderMailResponse = BaseResponse & {
|
||||
reminder_mail?: Partial<ReminderMail>;
|
||||
};
|
||||
export type ReminderMailsResponse = BaseResponse & {
|
||||
reminder_mails?: Partial<ReminderMail>[];
|
||||
};
|
||||
14
calcom/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts
Normal file
14
calcom/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||
}
|
||||
37
calcom/apps/api/v1/lib/utils/isAdmin.ts
Normal file
37
calcom/apps/api/v1/lib/utils/isAdmin.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide };
|
||||
|
||||
const orgOwnerOrAdminMemberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
};
|
||||
4
calcom/apps/api/v1/lib/utils/isValidBase64Image.ts
Normal file
4
calcom/apps/api/v1/lib/utils/isValidBase64Image.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function isValidBase64Image(input: string): boolean {
|
||||
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
return regex.test(input);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
type AccessibleUsersType = {
|
||||
memberUserIds: number[];
|
||||
adminUserId: number;
|
||||
};
|
||||
|
||||
const getAllOrganizationMemberships = async (
|
||||
memberships: {
|
||||
userId: number;
|
||||
role: MembershipRole;
|
||||
teamId: number;
|
||||
}[],
|
||||
orgId: number
|
||||
) => {
|
||||
return memberships.reduce<number[]>((acc, membership) => {
|
||||
if (membership.teamId === orgId) {
|
||||
acc.push(membership.userId);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const getAllAdminMemberships = async (userId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getAllOrganizationMembers = async (organizationId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId;
|
||||
if (!orgId) return [];
|
||||
|
||||
const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId);
|
||||
const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId);
|
||||
return accessibleUserIds;
|
||||
};
|
||||
|
||||
export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => {
|
||||
const adminMemberships = await getAllAdminMemberships(adminId);
|
||||
const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id;
|
||||
if (!organizationId) return [];
|
||||
|
||||
const allMemberships = await getAllOrganizationMembers(organizationId);
|
||||
return allMemberships.map((membership) => membership.userId);
|
||||
};
|
||||
4
calcom/apps/api/v1/lib/utils/scopeOfAdmin.ts
Normal file
4
calcom/apps/api/v1/lib/utils/scopeOfAdmin.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const ScopeOfAdmin = {
|
||||
SystemWide: "SystemWide",
|
||||
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
|
||||
} as const;
|
||||
4
calcom/apps/api/v1/lib/utils/stringifyISODate.ts
Normal file
4
calcom/apps/api/v1/lib/utils/stringifyISODate.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const stringifyISODate = (date: Date | undefined): string => {
|
||||
return `${date?.toISOString()}`;
|
||||
};
|
||||
// TODO: create a function that takes an object and returns a stringified version of dates of it.
|
||||
29
calcom/apps/api/v1/lib/validations/api-key.ts
Normal file
29
calcom/apps/api/v1/lib/validations/api-key.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _ApiKeyModel as ApiKey } from "@calcom/prisma/zod";
|
||||
|
||||
export const apiKeyCreateBodySchema = ApiKey.pick({
|
||||
note: true,
|
||||
expiresAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial({ userId: true })
|
||||
.merge(z.object({ neverExpires: z.boolean().optional() }))
|
||||
.strict();
|
||||
|
||||
export const apiKeyEditBodySchema = ApiKey.pick({
|
||||
note: true,
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
export const apiKeyPublicSchema = ApiKey.pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
expiresAt: true,
|
||||
lastUsedAt: true,
|
||||
/** We might never want to expose these. Leaving this a as reminder. */
|
||||
// hashedKey: true,
|
||||
});
|
||||
39
calcom/apps/api/v1/lib/validations/attendee.ts
Normal file
39
calcom/apps/api/v1/lib/validations/attendee.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
|
||||
|
||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||
|
||||
export const schemaAttendeeBaseBodyParams = Attendee.pick({
|
||||
bookingId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
});
|
||||
|
||||
const schemaAttendeeCreateParams = z
|
||||
.object({
|
||||
bookingId: z.number().int(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
timeZone: timeZone,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const schemaAttendeeEditParams = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
})
|
||||
.strict();
|
||||
export const schemaAttendeeEditBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeEditParams);
|
||||
export const schemaAttendeeCreateBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeCreateParams);
|
||||
|
||||
export const schemaAttendeeReadPublic = Attendee.pick({
|
||||
id: true,
|
||||
bookingId: true,
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
});
|
||||
56
calcom/apps/api/v1/lib/validations/availability.ts
Normal file
56
calcom/apps/api/v1/lib/validations/availability.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _AvailabilityModel as Availability, _ScheduleModel as Schedule } from "@calcom/prisma/zod";
|
||||
import { denullishShape } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const schemaAvailabilityBaseBodyParams = /** We make all these properties required */ denullishShape(
|
||||
Availability.pick({
|
||||
/** We need to pass the schedule where this availability belongs to */
|
||||
scheduleId: true,
|
||||
})
|
||||
);
|
||||
|
||||
export const schemaAvailabilityReadPublic = Availability.pick({
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
date: true,
|
||||
scheduleId: true,
|
||||
days: true,
|
||||
// eventTypeId: true /** @deprecated */,
|
||||
// userId: true /** @deprecated */,
|
||||
}).merge(z.object({ success: z.boolean().optional(), Schedule: Schedule.partial() }).partial());
|
||||
|
||||
const schemaAvailabilityCreateParams = z
|
||||
.object({
|
||||
startTime: z.date().or(z.string()),
|
||||
endTime: z.date().or(z.string()),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const schemaAvailabilityEditParams = z
|
||||
.object({
|
||||
startTime: z.date().or(z.string()).optional(),
|
||||
endTime: z.date().or(z.string()).optional(),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaAvailabilityEditBodyParams = schemaAvailabilityEditParams;
|
||||
|
||||
export const schemaAvailabilityCreateBodyParams = schemaAvailabilityBaseBodyParams.merge(
|
||||
schemaAvailabilityCreateParams
|
||||
);
|
||||
|
||||
export const schemaAvailabilityReadBodyParams = z
|
||||
.object({
|
||||
userId: z.union([z.number(), z.array(z.number())]),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const schemaSingleAvailabilityReadBodyParams = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
28
calcom/apps/api/v1/lib/validations/booking-reference.ts
Normal file
28
calcom/apps/api/v1/lib/validations/booking-reference.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { _BookingReferenceModel as BookingReference } from "@calcom/prisma/zod";
|
||||
import { denullishShape } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const schemaBookingReferenceBaseBodyParams = BookingReference.pick({
|
||||
type: true,
|
||||
bookingId: true,
|
||||
uid: true,
|
||||
meetingId: true,
|
||||
meetingPassword: true,
|
||||
meetingUrl: true,
|
||||
deleted: true,
|
||||
}).partial();
|
||||
|
||||
export const schemaBookingReferenceReadPublic = BookingReference.pick({
|
||||
id: true,
|
||||
type: true,
|
||||
bookingId: true,
|
||||
uid: true,
|
||||
meetingId: true,
|
||||
meetingPassword: true,
|
||||
meetingUrl: true,
|
||||
deleted: true,
|
||||
});
|
||||
|
||||
export const schemaBookingCreateBodyParams = BookingReference.omit({ id: true, bookingId: true })
|
||||
.merge(denullishShape(BookingReference.pick({ bookingId: true })))
|
||||
.strict();
|
||||
export const schemaBookingEditBodyParams = schemaBookingCreateBodyParams.partial();
|
||||
88
calcom/apps/api/v1/lib/validations/booking.ts
Normal file
88
calcom/apps/api/v1/lib/validations/booking.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod";
|
||||
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
|
||||
const schemaBookingBaseBodyParams = Booking.pick({
|
||||
uid: true,
|
||||
userId: true,
|
||||
eventTypeId: true,
|
||||
title: true,
|
||||
description: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
status: true,
|
||||
}).partial();
|
||||
|
||||
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
|
||||
|
||||
export const schemaBookingGetParams = z.object({
|
||||
dateFrom: iso8601.optional(),
|
||||
dateTo: iso8601.optional(),
|
||||
order: z.enum(["asc", "desc"]).default("asc"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional(),
|
||||
});
|
||||
|
||||
const schemaBookingEditParams = z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
startTime: iso8601.optional(),
|
||||
endTime: iso8601.optional(),
|
||||
// Not supporting responses in edit as that might require re-triggering emails
|
||||
// responses
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
|
||||
.merge(schemaBookingEditParams)
|
||||
.omit({ uid: true });
|
||||
|
||||
export const schemaBookingReadPublic = Booking.extend({
|
||||
attendees: z
|
||||
.array(
|
||||
_AttendeeModel.pick({
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
user: _UserModel
|
||||
.pick({
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
})
|
||||
.nullish(),
|
||||
payment: z
|
||||
.array(
|
||||
_PaymentModel.pick({
|
||||
id: true,
|
||||
success: true,
|
||||
paymentOption: true,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
responses: z.record(z.any()).nullable(),
|
||||
}).pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
description: true,
|
||||
eventTypeId: true,
|
||||
uid: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
timeZone: true,
|
||||
attendees: true,
|
||||
user: true,
|
||||
payment: true,
|
||||
metadata: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
fromReschedule: true,
|
||||
});
|
||||
18
calcom/apps/api/v1/lib/validations/connected-calendar.ts
Normal file
18
calcom/apps/api/v1/lib/validations/connected-calendar.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const CalendarSchema = z.object({
|
||||
externalId: z.string(),
|
||||
name: z.string(),
|
||||
primary: z.boolean(),
|
||||
readOnly: z.boolean(),
|
||||
});
|
||||
|
||||
const IntegrationSchema = z.object({
|
||||
name: z.string(),
|
||||
appId: z.string(),
|
||||
userId: z.number(),
|
||||
integration: z.string(),
|
||||
calendars: z.array(CalendarSchema),
|
||||
});
|
||||
|
||||
export const schemaConnectedCalendarsReadPublic = z.array(IntegrationSchema);
|
||||
64
calcom/apps/api/v1/lib/validations/credential-sync.ts
Normal file
64
calcom/apps/api/v1/lib/validations/credential-sync.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
const userId = z.string().transform((val) => {
|
||||
const userIdInt = parseInt(val);
|
||||
|
||||
if (isNaN(userIdInt)) {
|
||||
throw new HttpError({ message: "userId is not a valid number", statusCode: 400 });
|
||||
}
|
||||
|
||||
return userIdInt;
|
||||
});
|
||||
const appSlug = z.string();
|
||||
const credentialId = z.string().transform((val) => {
|
||||
const credentialIdInt = parseInt(val);
|
||||
|
||||
if (isNaN(credentialIdInt)) {
|
||||
throw new HttpError({ message: "credentialId is not a valid number", statusCode: 400 });
|
||||
}
|
||||
|
||||
return credentialIdInt;
|
||||
});
|
||||
const encryptedKey = z.string();
|
||||
|
||||
export const schemaCredentialGetParams = z.object({
|
||||
userId,
|
||||
appSlug: appSlug.optional(),
|
||||
});
|
||||
|
||||
export const schemaCredentialPostParams = z.object({
|
||||
userId,
|
||||
createSelectedCalendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
return val === "true";
|
||||
}),
|
||||
createDestinationCalendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
return val === "true";
|
||||
}),
|
||||
});
|
||||
|
||||
export const schemaCredentialPostBody = z.object({
|
||||
appSlug,
|
||||
encryptedKey,
|
||||
});
|
||||
|
||||
export const schemaCredentialPatchParams = z.object({
|
||||
userId,
|
||||
credentialId,
|
||||
});
|
||||
|
||||
export const schemaCredentialPatchBody = z.object({
|
||||
encryptedKey,
|
||||
});
|
||||
|
||||
export const schemaCredentialDeleteParams = z.object({
|
||||
userId,
|
||||
credentialId,
|
||||
});
|
||||
48
calcom/apps/api/v1/lib/validations/destination-calendar.ts
Normal file
48
calcom/apps/api/v1/lib/validations/destination-calendar.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
|
||||
const schemaDestinationCalendarCreateParams = z
|
||||
.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
eventTypeId: z.number().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaDestinationCalendarCreateBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
|
||||
schemaDestinationCalendarCreateParams
|
||||
);
|
||||
|
||||
const schemaDestinationCalendarEditParams = z
|
||||
.object({
|
||||
integration: z.string().optional(),
|
||||
externalId: z.string().optional(),
|
||||
eventTypeId: z.number().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
|
||||
schemaDestinationCalendarEditParams
|
||||
);
|
||||
|
||||
export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
|
||||
id: true,
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { _EventTypeCustomInputModel as EventTypeCustomInput } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaEventTypeCustomInputBaseBodyParams = EventTypeCustomInput.omit({
|
||||
id: true,
|
||||
});
|
||||
|
||||
export const schemaEventTypeCustomInputPublic = EventTypeCustomInput.omit({});
|
||||
|
||||
export const schemaEventTypeCustomInputBodyParams = schemaEventTypeCustomInputBaseBodyParams.strict();
|
||||
|
||||
export const schemaEventTypeCustomInputEditBodyParams = schemaEventTypeCustomInputBaseBodyParams
|
||||
.partial()
|
||||
.strict();
|
||||
173
calcom/apps/api/v1/lib/validations/event-type.ts
Normal file
173
calcom/apps/api/v1/lib/validations/event-type.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
|
||||
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { Frequency } from "~/lib/types";
|
||||
|
||||
import { jsonSchema } from "./shared/jsonSchema";
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
import { timeZone } from "./shared/timeZone";
|
||||
|
||||
const recurringEventInputSchema = z.object({
|
||||
dtstart: z.string().optional(),
|
||||
interval: z.number().int().optional(),
|
||||
count: z.number().int().optional(),
|
||||
freq: z.nativeEnum(Frequency).optional(),
|
||||
until: z.string().optional(),
|
||||
tzid: timeZone.optional(),
|
||||
});
|
||||
|
||||
const hostSchema = _HostModel.pick({
|
||||
isFixed: true,
|
||||
userId: true,
|
||||
});
|
||||
|
||||
export const childrenSchema = z.object({
|
||||
id: z.number().int(),
|
||||
userId: z.number().int(),
|
||||
});
|
||||
|
||||
export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
title: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
schedulingType: true,
|
||||
// START Limit future bookings
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
// END Limit future bookings
|
||||
requiresConfirmation: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
parentId: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
teamId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
assignAllTeamMembers: true,
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
})
|
||||
)
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
const schemaEventTypeCreateParams = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
description: z.string().optional().nullable(),
|
||||
length: z.number().int(),
|
||||
metadata: z.any().optional(),
|
||||
recurringEvent: recurringEventInputSchema.optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
parentId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams
|
||||
.merge(schemaEventTypeCreateParams)
|
||||
.merge(schemaQueryUserId.partial());
|
||||
|
||||
const schemaEventTypeEditParams = z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.transform((s) => slugify(s))
|
||||
.optional(),
|
||||
length: z.number().int().optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
|
||||
export const schemaEventTypeReadPublic = EventType.pick({
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
locations: z
|
||||
.array(
|
||||
z.object({
|
||||
link: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
hostPhoneNumber: z.string().optional(),
|
||||
type: z.any().optional(),
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
metadata: jsonSchema.nullable(),
|
||||
customInputs: customInputSchema.array().optional(),
|
||||
link: z.string().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||
})
|
||||
);
|
||||
68
calcom/apps/api/v1/lib/validations/membership.ts
Normal file
68
calcom/apps/api/v1/lib/validations/membership.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { _MembershipModel as Membership, _TeamModel } from "@calcom/prisma/zod";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
export const schemaMembershipBaseBodyParams = Membership.omit({});
|
||||
|
||||
const schemaMembershipRequiredParams = z.object({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const membershipCreateBodySchema = Membership.omit({ id: true })
|
||||
.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
})
|
||||
.transform((v) => ({
|
||||
accepted: false,
|
||||
role: MembershipRole.MEMBER,
|
||||
disableImpersonation: false,
|
||||
...v,
|
||||
}));
|
||||
|
||||
export const membershipEditBodySchema = Membership.omit({
|
||||
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
|
||||
teamId: true,
|
||||
userId: true,
|
||||
id: true,
|
||||
})
|
||||
.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaMembershipBodyParams = schemaMembershipBaseBodyParams.merge(
|
||||
schemaMembershipRequiredParams
|
||||
);
|
||||
|
||||
export const schemaMembershipPublic = Membership.merge(z.object({ team: _TeamModel }).partial());
|
||||
|
||||
/** We extract userId and teamId from compound ID string */
|
||||
export const membershipIdSchema = schemaQueryIdAsString
|
||||
// So we can query additional team data in memberships
|
||||
.merge(z.object({ teamId: z.union([stringOrNumber, z.array(stringOrNumber)]) }).partial())
|
||||
.transform((v, ctx) => {
|
||||
const [userIdStr, teamIdStr] = v.id.split("_");
|
||||
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
|
||||
const teamIdInt = schemaQueryIdParseInt.safeParse({ id: teamIdStr });
|
||||
if (!userIdInt.success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
|
||||
return z.NEVER;
|
||||
}
|
||||
if (!teamIdInt.success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "teamId is not a number " });
|
||||
return z.NEVER;
|
||||
}
|
||||
return {
|
||||
userId: userIdInt.data.id,
|
||||
teamId: teamIdInt.data.id,
|
||||
};
|
||||
});
|
||||
5
calcom/apps/api/v1/lib/validations/payment.ts
Normal file
5
calcom/apps/api/v1/lib/validations/payment.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
|
||||
|
||||
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
|
||||
export const schemaPaymentBodyParams = Payment.omit({ id: true });
|
||||
export const schemaPaymentPublic = Payment.omit({ externalId: true });
|
||||
17
calcom/apps/api/v1/lib/validations/reminder-mail.ts
Normal file
17
calcom/apps/api/v1/lib/validations/reminder-mail.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _ReminderMailModel as ReminderMail } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaReminderMailBaseBodyParams = ReminderMail.omit({ id: true }).partial();
|
||||
|
||||
export const schemaReminderMailPublic = ReminderMail.omit({});
|
||||
|
||||
const schemaReminderMailRequiredParams = z.object({
|
||||
referenceId: z.number().int(),
|
||||
reminderType: z.enum(["PENDING_BOOKING_CONFIRMATION"]),
|
||||
elapsedMinutes: z.number().int(),
|
||||
});
|
||||
|
||||
export const schemaReminderMailBodyParams = schemaReminderMailBaseBodyParams.merge(
|
||||
schemaReminderMailRequiredParams
|
||||
);
|
||||
43
calcom/apps/api/v1/lib/validations/schedule.ts
Normal file
43
calcom/apps/api/v1/lib/validations/schedule.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { _ScheduleModel as Schedule, _AvailabilityModel as Availability } from "@calcom/prisma/zod";
|
||||
|
||||
import { timeZone } from "./shared/timeZone";
|
||||
|
||||
const schemaScheduleBaseBodyParams = Schedule.omit({ id: true, timeZone: true }).partial();
|
||||
|
||||
export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
|
||||
z.object({ userId: z.number().optional(), timeZone: timeZone.optional() })
|
||||
);
|
||||
|
||||
export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
|
||||
z.object({ userId: z.number().optional(), name: z.string(), timeZone })
|
||||
);
|
||||
|
||||
export const schemaSchedulePublic = z
|
||||
.object({ id: z.number() })
|
||||
.merge(Schedule)
|
||||
.merge(
|
||||
z.object({
|
||||
availability: z
|
||||
.array(
|
||||
Availability.pick({
|
||||
id: true,
|
||||
eventTypeId: true,
|
||||
date: true,
|
||||
days: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
})
|
||||
)
|
||||
.transform((v) =>
|
||||
v.map((item) => ({
|
||||
...item,
|
||||
startTime: dayjs.utc(item.startTime).format("HH:mm:ss"),
|
||||
endTime: dayjs.utc(item.endTime).format("HH:mm:ss"),
|
||||
}))
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
);
|
||||
48
calcom/apps/api/v1/lib/validations/selected-calendar.ts
Normal file
48
calcom/apps/api/v1/lib/validations/selected-calendar.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import z from "zod";
|
||||
|
||||
import { _SelectedCalendarModel as SelectedCalendar } from "@calcom/prisma/zod";
|
||||
|
||||
import { schemaQueryIdAsString } from "./shared/queryIdString";
|
||||
import { schemaQueryIdParseInt } from "./shared/queryIdTransformParseInt";
|
||||
|
||||
export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
|
||||
|
||||
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
|
||||
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
|
||||
userId: true,
|
||||
});
|
||||
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
|
||||
|
||||
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
|
||||
/** We can assume the first part is the userId since it's an integer */
|
||||
const [userIdStr, ...rest] = v.id.split("_");
|
||||
/** We can assume that the remainder is both the integration type and external id combined */
|
||||
const integration_externalId = rest.join("_");
|
||||
/**
|
||||
* Since we only handle calendars here we can split by `_calendar_` and re add it later on.
|
||||
* This handle special cases like `google_calendar_c_blabla@group.calendar.google.com` and
|
||||
* `hubspot_other_calendar`.
|
||||
**/
|
||||
const [_integration, externalId] = integration_externalId.split("_calendar_");
|
||||
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
|
||||
if (!userIdInt.success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
|
||||
return z.NEVER;
|
||||
}
|
||||
if (!_integration) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing integration" });
|
||||
return z.NEVER;
|
||||
}
|
||||
if (!externalId) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing externalId" });
|
||||
return z.NEVER;
|
||||
}
|
||||
return {
|
||||
userId: userIdInt.data.id,
|
||||
/** We re-add the split `_calendar` string */
|
||||
integration: `${_integration}_calendar`,
|
||||
externalId,
|
||||
};
|
||||
});
|
||||
11
calcom/apps/api/v1/lib/validations/shared/baseApiParams.ts
Normal file
11
calcom/apps/api/v1/lib/validations/shared/baseApiParams.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const baseApiParams = z.object({
|
||||
// since we added apiKey as query param this is required by next-validations helper
|
||||
// for query params to work properly and not fail.
|
||||
apiKey: z.string().optional(),
|
||||
// version required for supporting /v1/ redirect to query in api as *?version=1
|
||||
version: z.string().optional(),
|
||||
});
|
||||
9
calcom/apps/api/v1/lib/validations/shared/jsonSchema.ts
Normal file
9
calcom/apps/api/v1/lib/validations/shared/jsonSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
export const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryAttendeeEmail = baseApiParams.extend({
|
||||
attendeeEmail: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
|
||||
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
|
||||
});
|
||||
|
||||
export const withValidQueryAttendeeEmail = withValidation({
|
||||
schema: schemaQueryAttendeeEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
19
calcom/apps/api/v1/lib/validations/shared/queryIdString.ts
Normal file
19
calcom/apps/api/v1/lib/validations/shared/queryIdString.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
/** Used for UUID style id queries */
|
||||
export const schemaQueryIdAsString = baseApiParams
|
||||
.extend({
|
||||
id: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const withValidQueryIdString = withValidation({
|
||||
schema: schemaQueryIdAsString,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryIdParseInt = baseApiParams.extend({
|
||||
id: z.coerce.number(),
|
||||
});
|
||||
|
||||
export const withValidQueryIdTransformParseInt = withValidation({
|
||||
schema: schemaQueryIdParseInt,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
|
||||
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||
recordingId: z.string(),
|
||||
});
|
||||
7
calcom/apps/api/v1/lib/validations/shared/querySlug.ts
Normal file
7
calcom/apps/api/v1/lib/validations/shared/querySlug.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
export const schemaQuerySlug = baseApiParams.extend({
|
||||
slug: z.string().optional(),
|
||||
});
|
||||
21
calcom/apps/api/v1/lib/validations/shared/queryTeamId.ts
Normal file
21
calcom/apps/api/v1/lib/validations/shared/queryTeamId.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryTeamId = baseApiParams
|
||||
.extend({
|
||||
teamId: z
|
||||
.string()
|
||||
.regex(/^\d+$/)
|
||||
.transform((id) => parseInt(id)),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const withValidQueryTeamId = withValidation({
|
||||
schema: schemaQueryTeamId,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
20
calcom/apps/api/v1/lib/validations/shared/queryUserEmail.ts
Normal file
20
calcom/apps/api/v1/lib/validations/shared/queryUserEmail.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryUserEmail = baseApiParams.extend({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserEmails = z.object({
|
||||
email: z.union([z.string().email(), z.array(z.string().email())]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserEmail = withValidation({
|
||||
schema: schemaQueryUserEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
26
calcom/apps/api/v1/lib/validations/shared/queryUserId.ts
Normal file
26
calcom/apps/api/v1/lib/validations/shared/queryUserId.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryUserId = baseApiParams.extend({
|
||||
userId: stringOrNumber,
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserIds = z.object({
|
||||
userId: z.union([stringOrNumber, z.array(stringOrNumber)]),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleTeamIds = z.object({
|
||||
teamId: z.union([stringOrNumber, z.array(stringOrNumber)]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserId = withValidation({
|
||||
schema: schemaQueryUserId,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
7
calcom/apps/api/v1/lib/validations/shared/timeZone.ts
Normal file
7
calcom/apps/api/v1/lib/validations/shared/timeZone.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import tzdata from "tzdata";
|
||||
import { z } from "zod";
|
||||
|
||||
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
|
||||
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
|
||||
message: `Expected one of the following: ${Object.keys(tzdata.zones).join(", ")}`,
|
||||
});
|
||||
30
calcom/apps/api/v1/lib/validations/team.ts
Normal file
30
calcom/apps/api/v1/lib/validations/team.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _TeamModel as Team } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true }).partial({
|
||||
hideBranding: true,
|
||||
metadata: true,
|
||||
pendingPayment: true,
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
smsLockState: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
name: z.string().max(255),
|
||||
});
|
||||
|
||||
export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict();
|
||||
|
||||
export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial();
|
||||
|
||||
const schemaOwnerId = z.object({
|
||||
ownerId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const schemaTeamCreateBodyParams = schemaTeamBodyParams.merge(schemaOwnerId).strict();
|
||||
|
||||
export const schemaTeamReadPublic = Team.omit({});
|
||||
|
||||
export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic);
|
||||
179
calcom/apps/api/v1/lib/validations/user.ts
Normal file
179
calcom/apps/api/v1/lib/validations/user.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
|
||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||
|
||||
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
||||
enum weekdays {
|
||||
MONDAY = "Monday",
|
||||
TUESDAY = "Tuesday",
|
||||
WEDNESDAY = "Wednesday",
|
||||
THURSDAY = "Thursday",
|
||||
FRIDAY = "Friday",
|
||||
SATURDAY = "Saturday",
|
||||
SUNDAY = "Sunday",
|
||||
}
|
||||
|
||||
// @note: extracted from apps/web/next-i18next.config.js, update if new locales.
|
||||
enum locales {
|
||||
EN = "en",
|
||||
FR = "fr",
|
||||
IT = "it",
|
||||
RU = "ru",
|
||||
ES = "es",
|
||||
DE = "de",
|
||||
PT = "pt",
|
||||
RO = "ro",
|
||||
NL = "nl",
|
||||
PT_BR = "pt-BR",
|
||||
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
|
||||
KO = "ko",
|
||||
JA = "ja",
|
||||
PL = "pl",
|
||||
AR = "ar",
|
||||
IW = "iw",
|
||||
ZH_CN = "zh-CN",
|
||||
ZH_TW = "zh-TW",
|
||||
CS = "cs",
|
||||
SR = "sr",
|
||||
SV = "sv",
|
||||
VI = "vi",
|
||||
}
|
||||
enum theme {
|
||||
DARK = "dark",
|
||||
LIGHT = "light",
|
||||
}
|
||||
|
||||
enum timeFormat {
|
||||
TWELVE = 12,
|
||||
TWENTY_FOUR = 24,
|
||||
}
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.transform((v) => v.toLowerCase())
|
||||
// .refine(() => {})
|
||||
.superRefine(async (val, ctx) => {
|
||||
if (val) {
|
||||
const result = await checkUsername(val);
|
||||
if (!result.available) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "already_in_use_error" });
|
||||
if (result.premium) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "premium_username" });
|
||||
}
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model
|
||||
export const schemaUserBaseBodyParams = User.pick({
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
theme: true,
|
||||
appTheme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
hideBranding: true,
|
||||
timeFormat: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
role: true,
|
||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||
// avatar: true,
|
||||
}).partial();
|
||||
// @note: partial() is used to allow for the user to edit only the fields they want to edit making all optional,
|
||||
// if want to make any required do it in the schemaRequiredParams
|
||||
|
||||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||
// for example making weekStart only accept weekdays as input
|
||||
const schemaUserEditParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
.refine((id: number) => id > 0)
|
||||
.optional()
|
||||
.nullable(),
|
||||
locale: z.nativeEnum(locales).optional().nullable(),
|
||||
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
.refine((id: number) => id > 0)
|
||||
.optional()
|
||||
.nullable(),
|
||||
locale: z.nativeEnum(locales).optional(),
|
||||
createdDate: iso8601.optional(),
|
||||
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
|
||||
.merge(schemaUserEditParams)
|
||||
.omit({})
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
export const schemaUserCreateBodyParams = schemaUserBaseBodyParams
|
||||
.merge(schemaUserCreateParams)
|
||||
.omit({})
|
||||
.strict();
|
||||
|
||||
// @note: These are the values that are always returned when reading a user
|
||||
export const schemaUserReadPublic = User.pick({
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
endTime: true,
|
||||
bufferTime: true,
|
||||
appTheme: true,
|
||||
theme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
hideBranding: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
createdDate: true,
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
role: true,
|
||||
});
|
||||
|
||||
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);
|
||||
57
calcom/apps/api/v1/lib/validations/webhook.ts
Normal file
57
calcom/apps/api/v1/lib/validations/webhook.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
|
||||
import { _WebhookModel as Webhook } from "@calcom/prisma/zod";
|
||||
|
||||
const schemaWebhookBaseBodyParams = Webhook.pick({
|
||||
userId: true,
|
||||
eventTypeId: true,
|
||||
eventTriggers: true,
|
||||
active: true,
|
||||
subscriberUrl: true,
|
||||
payloadTemplate: true,
|
||||
});
|
||||
|
||||
export const schemaWebhookCreateParams = z
|
||||
.object({
|
||||
// subscriberUrl: z.string().url(),
|
||||
// eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(),
|
||||
// active: z.boolean(),
|
||||
payloadTemplate: z.string().optional().nullable(),
|
||||
eventTypeId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
secret: z.string().optional().nullable(),
|
||||
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
|
||||
// appId: z.string().optional().nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaWebhookCreateBodyParams = schemaWebhookBaseBodyParams.merge(schemaWebhookCreateParams);
|
||||
|
||||
export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams
|
||||
.merge(
|
||||
z.object({
|
||||
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
|
||||
secret: z.string().optional().nullable(),
|
||||
})
|
||||
)
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
export const schemaWebhookReadPublic = Webhook.pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
eventTypeId: true,
|
||||
payloadTemplate: true,
|
||||
eventTriggers: true,
|
||||
// FIXME: We have some invalid urls saved in the DB
|
||||
// subscriberUrl: true,
|
||||
/** @todo: find out how to properly add back and validate those. */
|
||||
// eventType: true,
|
||||
// app: true,
|
||||
appId: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
subscriberUrl: z.string(),
|
||||
})
|
||||
);
|
||||
Reference in New Issue
Block a user