Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be525a27df | ||
|
|
df037b008f | ||
|
|
8fb4df4b8c | ||
|
|
6758654796 | ||
|
|
1e86f1046b | ||
|
|
d3775cd920 | ||
|
|
3ab053bf17 | ||
|
|
4a4bee7140 | ||
|
|
9ba85ea0b2 | ||
|
|
d1d1472f3e | ||
|
|
8d18b3da38 | ||
|
|
631c5f7c53 | ||
|
|
f2ca2bdc83 | ||
|
|
bb36807689 | ||
|
|
7ed32d3c77 | ||
|
|
5d84179345 | ||
|
|
cb437231ad | ||
|
|
d47a5be659 | ||
|
|
e89211782c | ||
|
|
fd454061e7 | ||
|
|
277ce1c244 | ||
|
|
1848cd55b1 | ||
|
|
b0e3cc4dd2 | ||
|
|
b3e344d226 | ||
|
|
810f3b834c | ||
|
|
bf1eacfd9b | ||
|
|
c5c654c879 | ||
|
|
d1ce04edc1 | ||
|
|
1f1862e526 | ||
|
|
63dfc3376c | ||
|
|
3166db9bcf | ||
|
|
f8af9dc17e | ||
|
|
ae659b5176 | ||
|
|
33226f268d | ||
|
|
7e2a0d1a1b | ||
|
|
a385bd4d5f | ||
|
|
4b4bc28f1d | ||
|
|
316fba908b | ||
|
|
d40d282912 | ||
|
|
e81df6fdeb | ||
|
|
76420411b9 | ||
|
|
7ec1617dc2 | ||
|
|
870cadd81f | ||
|
|
04c581a25e | ||
|
|
5ef131333d | ||
|
|
c262220f0f | ||
|
|
b5e08ea80f | ||
|
|
00ee1ef441 | ||
|
|
6cdc9c9f59 | ||
|
|
56d1998f0f | ||
|
|
d59fdf21fd | ||
|
|
1a96149ce3 | ||
|
|
d2bc81758c | ||
|
|
0565e23818 |
@@ -1,3 +1,4 @@
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
||||
/deploy/**/* @calcom/Foundation
|
||||
/infra/**/* @calcom/Foundation
|
||||
|
||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -172,24 +172,25 @@ yarn dx
|
||||
```
|
||||
|
||||
#### Development tip
|
||||
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
|
||||
|
||||
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||
Where {level} can be one of the following:
|
||||
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||
Where {level} can be one of the following:
|
||||
|
||||
`0` for silly \
|
||||
`1` for trace \
|
||||
`2` for debug \
|
||||
`3` for info \
|
||||
`4` for warn \
|
||||
`5` for error \
|
||||
`6` for fatal
|
||||
`0` for silly \
|
||||
`1` for trace \
|
||||
`2` for debug \
|
||||
`3` for info \
|
||||
`4` for warn \
|
||||
`5` for error \
|
||||
`6` for fatal
|
||||
|
||||
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
|
||||
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
|
||||
|
||||
The logger will include all logs that are at the specified level or higher. For example: \
|
||||
The logger will include all logs that are at the specified level or higher. For example: \
|
||||
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
|
||||
|
||||
```sh
|
||||
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
_TeamModel,
|
||||
_UserModel,
|
||||
} from "@calcom/prisma/zod";
|
||||
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
|
||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||
import { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking";
|
||||
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.127",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.142",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -101,7 +101,7 @@
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "../../../packages/prisma/schema.prisma"
|
||||
|
||||
@@ -19,7 +19,7 @@ import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
@@ -442,7 +442,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
return keyData?.userId;
|
||||
} else {
|
||||
// Access Token
|
||||
return this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
return ownerId;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -208,7 +208,7 @@ export class ConnectedCalendar {
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
id!: number | string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
|
||||
+2
-2
@@ -33,10 +33,10 @@ import {
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
EventTypesByViewer,
|
||||
EventTypesPublic,
|
||||
eventTypeBookingFields,
|
||||
eventTypeLocations,
|
||||
} from "@calcom/platform-libraries";
|
||||
EventTypesPublic,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries/event-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+14
-3
@@ -1,6 +1,8 @@
|
||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
|
||||
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
EventTypesPublic,
|
||||
getEventTypesPublic,
|
||||
systemBeforeFieldEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
@@ -68,6 +70,7 @@ export class EventTypesService_2024_04_15 {
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,7 +137,7 @@ export class EventTypesService_2024_04_15 {
|
||||
!bookingFields.find((field) => field.type === "email") &&
|
||||
!bookingFields.find((field) => field.type === "phone")
|
||||
) {
|
||||
bookingFields.push(systemBeforeFieldEmail);
|
||||
bookingFields.push({ ...systemBeforeFieldEmail, type: BaseField.email, editable: Editable.system });
|
||||
}
|
||||
|
||||
await updateEventType({
|
||||
@@ -167,7 +170,15 @@ export class EventTypesService_2024_04_15 {
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
...user,
|
||||
locale: user.locale ?? "en",
|
||||
profile: { id: profileId },
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
|
||||
-1
@@ -1845,7 +1845,6 @@ describe("Event types Endpoints", () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
|
||||
+27
-5
@@ -8,9 +8,13 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
import {
|
||||
createEventType,
|
||||
updateEventType,
|
||||
getEventTypesPublic,
|
||||
EventTypesPublic,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@@ -112,8 +116,11 @@ export class EventTypesService_2024_06_14 {
|
||||
: false;
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
id: user.id,
|
||||
locale: user.locale ?? "en",
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
organizationId: user.organizationId,
|
||||
@@ -121,6 +128,9 @@ export class EventTypesService_2024_06_14 {
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
selectedCalendars,
|
||||
email: user.email,
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,7 +214,11 @@ export class EventTypesService_2024_06_14 {
|
||||
return defaultEventTypes;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
|
||||
async updateEventType(
|
||||
eventTypeId: number,
|
||||
body: Partial<InputEventTransformed_2024_06_14>,
|
||||
user: UserWithProfile
|
||||
) {
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
|
||||
@@ -230,7 +244,7 @@ export class EventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined | null) {
|
||||
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
@@ -242,7 +256,15 @@ export class EventTypesService_2024_06_14 {
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
...user,
|
||||
locale: user.locale ?? "en",
|
||||
profile: { id: profileId },
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
|
||||
+34
-10
@@ -25,13 +25,17 @@ import {
|
||||
transformSeatsApiToInternal,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries";
|
||||
InternalLocation,
|
||||
InternalLocationSchema,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
DestinationCalendar_2024_06_14,
|
||||
InputEventTransformed_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
@@ -58,7 +62,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
const transformedBody = this.transformInputCreateEventType(inputEventType);
|
||||
|
||||
await this.validateEventTypeInputs({
|
||||
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
|
||||
seatsPerTimeSlot: transformedBody?.seatsPerTimeSlot || null,
|
||||
locations: transformedBody.locations,
|
||||
requiresConfirmation: transformedBody.requiresConfirmation,
|
||||
eventName: transformedBody.eventName,
|
||||
@@ -272,7 +276,12 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
|
||||
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
|
||||
const layouts = transformBookerLayoutsApiToInternal(inputBookerLayouts);
|
||||
if (!layouts) return undefined;
|
||||
return {
|
||||
defaultLayout: layouts.defaultLayout as unknown as BookerLayouts,
|
||||
enabledLayouts: layouts.enabledLayouts as unknown as BookerLayouts[],
|
||||
};
|
||||
}
|
||||
|
||||
transformInputConfirmationPolicy(
|
||||
@@ -303,23 +312,22 @@ export class InputEventTypesService_2024_06_14 {
|
||||
eventName,
|
||||
}: ValidationContext) {
|
||||
let seatsPerTimeSlotDb: number | null = null;
|
||||
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
|
||||
let locationsDb: ReturnType<typeof this.transformLocations> = [];
|
||||
let requiresConfirmationDb = false;
|
||||
|
||||
if (eventTypeId != null) {
|
||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
|
||||
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
|
||||
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
|
||||
locationsDb = this.transformLocations(eventTypeDb?.locations) ?? [];
|
||||
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
|
||||
}
|
||||
|
||||
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
|
||||
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
|
||||
|
||||
const locationsFinal = locations !== undefined ? locations : locationsDb;
|
||||
const requiresConfirmationFinal =
|
||||
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
|
||||
|
||||
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
|
||||
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
|
||||
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
|
||||
@@ -331,7 +339,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
validateSeatsSingleLocationRule(
|
||||
seatsEnabled: boolean,
|
||||
locations: ReturnType<typeof this.transformInputLocations>
|
||||
locations: ReturnType<typeof this.transformLocations>
|
||||
) {
|
||||
if (seatsEnabled && locations.length > 1) {
|
||||
throw new BadRequestException(
|
||||
@@ -339,6 +347,22 @@ export class InputEventTypesService_2024_06_14 {
|
||||
);
|
||||
}
|
||||
}
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
for (const location of locations) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
knownLocations.push(result.data);
|
||||
} else {
|
||||
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
}
|
||||
}
|
||||
return [...knownLocations];
|
||||
}
|
||||
|
||||
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
|
||||
if (seatsEnabled && requiresConfirmation) {
|
||||
@@ -349,7 +373,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
validateMultipleLocationsSeatsDisabledRule(
|
||||
locations: ReturnType<typeof this.transformInputLocations>,
|
||||
locations: ReturnType<typeof this.transformLocations>,
|
||||
seatsEnabled: boolean
|
||||
) {
|
||||
if (locations.length > 1 && seatsEnabled) {
|
||||
|
||||
+11
-8
@@ -2,15 +2,18 @@ import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
||||
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
userMetadata,
|
||||
parseBookingLimit,
|
||||
parseRecurringEvent,
|
||||
getBookingFieldsWithSystemFields,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
transformLocationsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
parseRecurringEvent,
|
||||
InternalLocationSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
parseBookingLimit,
|
||||
transformIntervalLimitsInternalToApi,
|
||||
transformFutureBookingLimitsInternalToApi,
|
||||
transformRecurrenceInternalToApi,
|
||||
@@ -21,8 +24,7 @@ import {
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
getBookingFieldsWithSystemFields,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
@@ -198,13 +200,13 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
transformLocations(locationDb: any) {
|
||||
if (!locationDb) return [];
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
for (const location of locations) {
|
||||
for (const location of locationDb) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
knownLocations.push(result.data);
|
||||
@@ -315,6 +317,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
transformEventTypeColor(eventTypeColor: any) {
|
||||
if (!eventTypeColor) return undefined;
|
||||
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
|
||||
if (!parsedeventTypeColor) return undefined;
|
||||
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
|
||||
import { ConnectedApps } from "@calcom/platform-libraries";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
/*
|
||||
@@ -81,7 +81,7 @@ export class AtomsController {
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { teamId } = queryParams;
|
||||
|
||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user.id, user.name, teamId);
|
||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -124,7 +124,11 @@ export class AtomsController {
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeReturn
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
const eventType = await this.eventTypesService.updateEventType(
|
||||
eventTypeId,
|
||||
{ ...body, id: eventTypeId },
|
||||
user
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
@@ -140,7 +144,12 @@ export class AtomsController {
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Body() body: UpdateEventTypeReturn
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
|
||||
const eventType = await this.eventTypesService.updateTeamEventType(
|
||||
eventTypeId,
|
||||
{ ...body, id: eventTypeId },
|
||||
user,
|
||||
teamId
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
|
||||
@@ -10,17 +10,11 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
updateEventType,
|
||||
TUpdateEventTypeInputSchema,
|
||||
systemBeforeFieldEmail,
|
||||
getEventTypeById,
|
||||
getEnabledAppsFromCredentials,
|
||||
getAppFromSlug,
|
||||
MembershipRole,
|
||||
EventTypeMetaDataSchema,
|
||||
getClientSecretFromPayment,
|
||||
getBulkEventTypes,
|
||||
bulkUpdateEventsToDefaultLocation,
|
||||
enrichUserWithDelegationConferencingCredentialsWithoutOrgId,
|
||||
} from "@calcom/platform-libraries";
|
||||
import type {
|
||||
App,
|
||||
@@ -31,6 +25,17 @@ import type {
|
||||
TDependencyData,
|
||||
CredentialPayload,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
getBulkEventTypes,
|
||||
getEventTypeById,
|
||||
bulkUpdateEventsToDefaultLocation,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
updateEventType,
|
||||
TUpdateEventTypeInputSchema,
|
||||
systemBeforeFieldEmail,
|
||||
EventTypeMetaDataSchema,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type EnabledAppType = App & {
|
||||
@@ -104,7 +109,7 @@ export class EventTypesAtomService {
|
||||
}
|
||||
|
||||
const eventType = await updateEventType({
|
||||
input: { id: eventTypeId, ...body, bookingFields },
|
||||
input: { ...body, id: eventTypeId, bookingFields },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -117,7 +122,7 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
|
||||
@@ -134,7 +139,7 @@ export class EventTypesAtomService {
|
||||
}
|
||||
|
||||
const eventType = await updateEventType({
|
||||
input: { id: eventTypeId, ...body, bookingFields },
|
||||
input: { ...body, id: eventTypeId, bookingFields },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -147,10 +152,15 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
|
||||
async checkCanUpdateTeamEventType(
|
||||
userId: number,
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
scheduleId: number | null | undefined
|
||||
) {
|
||||
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
|
||||
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
|
||||
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
|
||||
@@ -179,11 +189,11 @@ export class EventTypesAtomService {
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypesAppIntegration(slug: string, userId: number, userName: string | null, teamId?: number) {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
|
||||
async getEventTypesAppIntegration(slug: string, user: UserWithProfile, teamId?: number) {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(user.id);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
if (teamId) {
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(user.id);
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
@@ -211,8 +221,17 @@ export class EventTypesAtomService {
|
||||
credentials = credentials.concat(teamAppCredentials);
|
||||
}
|
||||
}
|
||||
//TODO: enrich credentials for DelegationCredential
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
|
||||
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
|
||||
{
|
||||
user: {
|
||||
...user,
|
||||
credentials,
|
||||
},
|
||||
}
|
||||
);
|
||||
credentials = allCredentials;
|
||||
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
|
||||
where: { slug },
|
||||
});
|
||||
const apps = await Promise.all(
|
||||
@@ -265,7 +284,7 @@ export class EventTypesAtomService {
|
||||
});
|
||||
}
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: userName,
|
||||
name: user.name,
|
||||
teamId,
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries";
|
||||
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
@@ -12,6 +12,13 @@ import {
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
|
||||
export type TransformedCreateTeamEventTypeInput = Awaited<
|
||||
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputCreateTeamEventType"]>
|
||||
>;
|
||||
|
||||
export type TransformedUpdateTeamEventTypeInput = Awaited<
|
||||
ReturnType<InstanceType<typeof InputOrganizationsEventTypesService>["transformInputUpdateTeamEventType"]>
|
||||
>;
|
||||
@Injectable()
|
||||
export class InputOrganizationsEventTypesService {
|
||||
constructor(
|
||||
|
||||
+13
-6
@@ -1,5 +1,9 @@
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||
import {
|
||||
TransformedCreateTeamEventTypeInput,
|
||||
TransformedUpdateTeamEventTypeInput,
|
||||
} from "@/modules/organizations/event-types/services/input.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
@@ -7,8 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { createEventType } from "@calcom/platform-libraries";
|
||||
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import { createEventType } from "@calcom/platform-libraries/event-types";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsEventTypesService {
|
||||
@@ -26,14 +30,16 @@ export class OrganizationsEventTypesService {
|
||||
user: UserWithProfile,
|
||||
teamId: number,
|
||||
orgId: number,
|
||||
body: InputTeamEventTransformed_2024_06_14
|
||||
body: TransformedCreateTeamEventTypeInput
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { hosts, children, destinationCalendar, ...rest } = body;
|
||||
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: { teamId: teamId, ...rest },
|
||||
input: {
|
||||
teamId: teamId,
|
||||
...rest,
|
||||
},
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -57,6 +63,7 @@ export class OrganizationsEventTypesService {
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId || null },
|
||||
metadata: user.metadata,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +94,7 @@ export class OrganizationsEventTypesService {
|
||||
async updateTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: InputTeamEventTransformed_2024_06_14,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -62,6 +62,15 @@ export class SelectedCalendarsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getUserEventTypeSelectedCalendar(userId: number) {
|
||||
return this.dbRead.prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId,
|
||||
eventTypeId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addUserSelectedCalendar(
|
||||
userId: number,
|
||||
integration: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||
import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||
import { SlotsService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots.service";
|
||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
|
||||
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
@@ -12,8 +13,7 @@ import {
|
||||
VERSION_2024_06_11,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
||||
import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
|
||||
import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types";
|
||||
|
||||
@@ -156,7 +156,7 @@ export class SlotsController_2024_04_15 {
|
||||
async getAvailableSlots(
|
||||
@Query() query: GetAvailableSlotsInput_2024_04_15,
|
||||
@Req() req: ExpressRequest
|
||||
): Promise<ApiResponse<AvailableSlotsType>> {
|
||||
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
|
||||
const isTeamEvent = await this.slotsService.checkIfIsTeamEvent(query.eventTypeId);
|
||||
const availableSlots = await getAvailableSlots({
|
||||
input: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SlotFormat } from "@calcom/platform-enums";
|
||||
export type TimeSlots = {
|
||||
slots: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>;
|
||||
};
|
||||
type RangeSlots = {
|
||||
export type RangeSlots = {
|
||||
slots: Record<string, { startTime: string; endTime: string; attendees?: number; bookingUid?: string }[]>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { SlotsRepository_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.repository";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -33,7 +31,7 @@ export class SlotsService_2024_04_15 {
|
||||
}
|
||||
}
|
||||
|
||||
if (eventType && shouldReserveSlot) {
|
||||
if (eventType && shouldReserveSlot && !input._isDryRun) {
|
||||
await Promise.all(
|
||||
eventType.users.map((user) =>
|
||||
this.slotsRepo.upsertSelectedSlot(user.id, input, uid, eventType.seatsPerTimeSlot !== null)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
@@ -88,6 +88,11 @@ export class SlotsInputService_2024_09_04 {
|
||||
dateTime = dateTime.set({ hour: 23, minute: 59, second: 59 });
|
||||
}
|
||||
|
||||
return dateTime.toISO();
|
||||
const ISOEndTime = dateTime.toISO();
|
||||
if (ISOEndTime === null) {
|
||||
throw new BadRequestException("Invalid end date");
|
||||
}
|
||||
|
||||
return ISOEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
||||
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import {
|
||||
TransformedCreateTeamEventTypeInput,
|
||||
TransformedUpdateTeamEventTypeInput,
|
||||
} from "@/modules/organizations/event-types/services/input.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
||||
@@ -7,8 +11,7 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsEventTypesService {
|
||||
@@ -25,7 +28,7 @@ export class TeamsEventTypesService {
|
||||
async createTeamEventType(
|
||||
user: UserWithProfile,
|
||||
teamId: number,
|
||||
body: InputTeamEventTransformed_2024_06_14
|
||||
body: TransformedCreateTeamEventTypeInput
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -62,6 +65,7 @@ export class TeamsEventTypesService {
|
||||
organization: { id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null },
|
||||
profile: { id: profileId || null },
|
||||
metadata: user.metadata,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,14 +104,16 @@ export class TeamsEventTypesService {
|
||||
async updateTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: InputTeamEventTransformed_2024_06_14,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
await this.validateEventTypeExists(teamId, eventTypeId);
|
||||
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
|
||||
|
||||
await updateEventType({
|
||||
input: { id: eventTypeId, ...body },
|
||||
input: {
|
||||
id: eventTypeId,
|
||||
...body,
|
||||
},
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -15999,7 +15999,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
"type": "object"
|
||||
},
|
||||
"integration": {
|
||||
"type": "string"
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
|
||||
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
|
||||
"@calcom/platform-utils": ["../../../packages/platform/utils/index.ts"],
|
||||
"@calcom/platform-enums": ["../../../packages/platform/enums/index.ts"]
|
||||
"@calcom/platform-enums": ["../../../packages/platform/enums/index.ts"],
|
||||
"@calcom/platform-libraries/event-types": ["../../../packages/platform/libraries/event-types.ts"],
|
||||
"@calcom/platform-libraries/slots": ["../../../packages/platform/libraries/slots.ts"]
|
||||
},
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import VerifyEmailPage from "~/auth/verify-email-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("verify_email"),
|
||||
(t) => t("verify_email_button"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
+14
-13
@@ -1,22 +1,23 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(req.body?.email);
|
||||
async function handler(req: NextRequest) {
|
||||
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse((await req.json())?.email);
|
||||
|
||||
if (!email.success) {
|
||||
return res.status(400).json({ message: "email is required" });
|
||||
return NextResponse.json({ message: "email is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// fallback to email if ip is not present
|
||||
let ip = (req.headers["x-real-ip"] as string) ?? email.data;
|
||||
let ip = (req.headers.get("x-real-ip") as string) ?? email.data;
|
||||
|
||||
const forwardedFor = req.headers["x-forwarded-for"] as string;
|
||||
const forwardedFor = req.headers.get("x-forwarded-for") as string;
|
||||
if (!ip && forwardedFor) {
|
||||
ip = forwardedFor?.split(",").at(0) ?? email.data;
|
||||
}
|
||||
@@ -34,15 +35,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
select: { name: true, email: true, locale: true },
|
||||
});
|
||||
// Don't leak info about whether the user exists
|
||||
if (!user) return res.status(201).json({ message: "password_reset_email_sent" });
|
||||
if (!user) return NextResponse.json({ message: "password_reset_email_sent" }, { status: 201 });
|
||||
await passwordResetRequest(user);
|
||||
return res.status(201).json({ message: "password_reset_email_sent" });
|
||||
return NextResponse.json({ message: "password_reset_email_sent" }, { status: 201 });
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
return res.status(500).json({ message: "Unable to create password reset request" });
|
||||
return NextResponse.json({ message: "Unable to create password reset request" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: handler }),
|
||||
});
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const requiredScopes = ["READ_PROFILE"];
|
||||
const token = req.headers.get("authorization")?.split(" ")[1] || "";
|
||||
const account = await isAuthorized(token, requiredScopes);
|
||||
|
||||
if (!account) {
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ username: account.name }, { status: 201 });
|
||||
}
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
+10
-19
@@ -1,23 +1,16 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).json({ message: "Invalid method" });
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshToken = req.headers.authorization?.split(" ")[1] || "";
|
||||
|
||||
const { client_id, client_secret, grant_type } = req.body;
|
||||
export async function POST(req: NextRequest) {
|
||||
const { client_id, client_secret, grant_type } = await req.json();
|
||||
|
||||
if (grant_type !== "refresh_token") {
|
||||
res.status(400).json({ message: "grant type invalid" });
|
||||
return;
|
||||
return NextResponse.json({ message: "grant type invalid" }, { status: 400 });
|
||||
}
|
||||
|
||||
const [hashedSecret] = generateSecret(client_secret);
|
||||
@@ -33,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
@@ -42,15 +34,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
let decodedRefreshToken: OAuthTokenPayload;
|
||||
|
||||
try {
|
||||
const refreshToken = req.headers.get("authorization")?.split(" ")[1] || "";
|
||||
decodedRefreshToken = jwt.verify(refreshToken, secretKey) as OAuthTokenPayload;
|
||||
} catch {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!decodedRefreshToken || decodedRefreshToken.token_type !== "Refresh Token") {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload: OAuthTokenPayload = {
|
||||
@@ -65,5 +56,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
expiresIn: 1800, // 30 min
|
||||
});
|
||||
|
||||
res.status(200).json({ access_token });
|
||||
return NextResponse.json({ access_token }, { status: 200 });
|
||||
}
|
||||
+9
-16
@@ -1,21 +1,16 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).json({ message: "Invalid method" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { code, client_id, client_secret, grant_type, redirect_uri } = req.body;
|
||||
export async function POST(req: NextRequest) {
|
||||
const { code, client_id, client_secret, grant_type, redirect_uri } = await req.json();
|
||||
|
||||
if (grant_type !== "authorization_code") {
|
||||
res.status(400).json({ message: "grant_type invalid" });
|
||||
return;
|
||||
return NextResponse.json({ message: "grant_type invalid" }, { status: 400 });
|
||||
}
|
||||
|
||||
const [hashedSecret] = generateSecret(client_secret);
|
||||
@@ -31,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
if (!client || client.redirectUri !== redirect_uri) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const accessCode = await prisma.accessCode.findFirst({
|
||||
@@ -45,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
|
||||
//delete all expired accessCodes + the one that is used here
|
||||
// Delete all expired accessCodes + the one that is used here
|
||||
await prisma.accessCode.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
@@ -63,8 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
if (!accessCode) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
@@ -93,5 +86,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
expiresIn: 30 * 24 * 60 * 60, // 30 days
|
||||
});
|
||||
|
||||
res.status(200).json({ access_token, refresh_token });
|
||||
return NextResponse.json({ access_token, refresh_token }, { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
// This is the callback endpoint for the OIDC provider
|
||||
// A team must set this endpoint in the OIDC provider's configuration
|
||||
async function handler(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
|
||||
if (!code || !state) {
|
||||
return NextResponse.json({ message: "Code and state are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
try {
|
||||
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
|
||||
|
||||
if (!redirect_url) {
|
||||
throw new HttpError({
|
||||
message: "No redirect URL found",
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.redirect(redirect_url, 302);
|
||||
} catch (err) {
|
||||
const { message, statusCode = 500 } = err as HttpError;
|
||||
|
||||
return NextResponse.json({ message }, { status: statusCode });
|
||||
}
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
+10
-8
@@ -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 { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
@@ -13,11 +14,10 @@ const passwordResetRequestSchema = z.object({
|
||||
requestId: z.string(), // format doesn't matter.
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Bad Method when not POST
|
||||
if (req.method !== "POST") return res.status(405).end();
|
||||
|
||||
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(req.body);
|
||||
async function handler(req: NextRequest) {
|
||||
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(
|
||||
await req.json()
|
||||
);
|
||||
// rate-limited there is a low, very low chance that a password request stays valid long enough
|
||||
// to brute force 3.8126967e+40 options.
|
||||
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
|
||||
@@ -54,12 +54,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return res.status(404).end();
|
||||
return NextResponse.json({}, { status: 404 });
|
||||
}
|
||||
|
||||
await expireResetPasswordRequest(rawRequestId);
|
||||
|
||||
return res.status(201).json({ message: "Password reset." });
|
||||
return NextResponse.json({ message: "Password reset." }, { status: 201 });
|
||||
}
|
||||
|
||||
async function expireResetPasswordRequest(rawRequestId: string) {
|
||||
@@ -73,3 +73,5 @@ async function expireResetPasswordRequest(rawRequestId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { handler as POST };
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { OAuthReq } from "@boxyhq/saml-jackson";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
try {
|
||||
const { redirect_url } = await oauthController.authorize(
|
||||
Object.fromEntries(req.nextUrl.searchParams) as unknown as OAuthReq
|
||||
);
|
||||
|
||||
return NextResponse.redirect(redirect_url as string, 302);
|
||||
} catch (err) {
|
||||
const { message, statusCode = 500 } = err as HttpError;
|
||||
|
||||
return NextResponse.json({ message }, { status: statusCode });
|
||||
}
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
const { redirect_url } = await oauthController.samlResponse(await req.json());
|
||||
|
||||
if (redirect_url) {
|
||||
return NextResponse.redirect(redirect_url, 302);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: "No redirect URL provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
const tokenResponse = await oauthController.token(await req.json());
|
||||
return NextResponse.json(tokenResponse);
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
const extractAuthToken = (req: NextRequest) => {
|
||||
const authHeader = req.headers.get("authorization");
|
||||
const parts = (authHeader || "").split(" ");
|
||||
if (parts.length > 1) return parts[1];
|
||||
|
||||
// check for query param
|
||||
let arr: string[] = [];
|
||||
const { access_token } = requestQuery.parse(Object.fromEntries(req.nextUrl.searchParams));
|
||||
arr = arr.concat(access_token);
|
||||
if (arr[0].length > 0) return arr[0];
|
||||
|
||||
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
};
|
||||
|
||||
const requestQuery = z.object({
|
||||
access_token: z.string(),
|
||||
});
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
const token = extractAuthToken(req);
|
||||
const userInfo = await oauthController.userInfo(token);
|
||||
return NextResponse.json(userInfo);
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
@@ -24,13 +24,13 @@ const querySchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
async function handler(req: NextRequest) {
|
||||
const userCount = await prisma.user.count();
|
||||
if (userCount !== 0) {
|
||||
throw new HttpError({ statusCode: 400, message: "No setup needed." });
|
||||
}
|
||||
|
||||
const parsedQuery = querySchema.safeParse(req.body);
|
||||
const parsedQuery = querySchema.safeParse(await req.json());
|
||||
if (!parsedQuery.success) {
|
||||
throw new HttpError({ statusCode: 422, message: parsedQuery.error.message });
|
||||
}
|
||||
@@ -54,9 +54,9 @@ async function handler(req: NextApiRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "First admin user created successfully." };
|
||||
return NextResponse.json({ message: "First admin user created successfully." });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
+29
-28
@@ -1,4 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
@@ -8,68 +10,68 @@ import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/client";
|
||||
|
||||
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";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
const session = await getServerSession({ req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!session.user?.id) {
|
||||
console.error("Session is missing a user id.");
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!user.twoFactorEnabled) {
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
return NextResponse.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
const isCorrectPassword = await verifyPassword(body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// if user has 2fa and using backup code
|
||||
if (user.twoFactorEnabled && req.body.backupCode) {
|
||||
// If user has 2FA and using backup code
|
||||
if (user.twoFactorEnabled && body.backupCode) {
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error("Missing encryption key; cannot proceed with backup code login.");
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
if (!user.backupCodes) {
|
||||
return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
|
||||
return NextResponse.json({ error: ErrorCode.MissingBackupCodes }, { status: 400 });
|
||||
}
|
||||
|
||||
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));
|
||||
|
||||
// check if user-supplied code matches one
|
||||
const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
|
||||
const index = backupCodes.indexOf(body.backupCode.replaceAll("-", ""));
|
||||
if (index === -1) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
|
||||
return NextResponse.json({ error: ErrorCode.IncorrectBackupCode }, { status: 400 });
|
||||
}
|
||||
|
||||
// we delete all stored backup codes at the end, no need to do this here
|
||||
|
||||
// if user has 2fa and NOT using backup code, try totp
|
||||
} else if (user.twoFactorEnabled) {
|
||||
if (!req.body.code) {
|
||||
return res.status(400).json({ error: ErrorCode.SecondFactorRequired });
|
||||
// throw new Error(ErrorCode.SecondFactorRequired);
|
||||
if (!body.code) {
|
||||
return NextResponse.json({ error: ErrorCode.SecondFactorRequired }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
@@ -78,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
|
||||
console.error("Missing encryption key; cannot proceed with two factor login.");
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
@@ -91,14 +93,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// If user has 2fa enabled, check if body.code is correct
|
||||
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||
const isValidToken = totpAuthenticatorCheck(body.code, secret);
|
||||
if (!isValidToken) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||
|
||||
// throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||
return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 });
|
||||
}
|
||||
}
|
||||
// If it is, disable users 2fa
|
||||
|
||||
// Disable 2FA
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
@@ -110,5 +111,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
return NextResponse.json({ message: "Two factor disabled" });
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!session.user?.id) {
|
||||
console.error("Session is missing a user id.");
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
return NextResponse.json({ error: ErrorCode.TwoFactorSetupRequired }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error("Missing encryption key; cannot proceed with two factor setup.");
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
if (secret.length !== 32) {
|
||||
console.error(
|
||||
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
|
||||
);
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const isValidToken = totpAuthenticatorCheck(body.code, secret);
|
||||
if (!isValidToken) {
|
||||
return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Two-factor enabled" });
|
||||
}
|
||||
+20
-17
@@ -1,5 +1,7 @@
|
||||
import crypto from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { authenticator } from "otplib";
|
||||
import qrcode from "qrcode";
|
||||
|
||||
@@ -10,54 +12,55 @@ import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
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";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
const session = await getServerSession({ req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!session.user?.id) {
|
||||
console.error("Session is missing a user id.");
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !user.password?.hash) {
|
||||
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
|
||||
return NextResponse.json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!user.password?.hash) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 });
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
|
||||
return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error("Missing encryption key; cannot proceed with two factor setup.");
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 });
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
const isCorrectPassword = await verifyPassword(body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 });
|
||||
}
|
||||
|
||||
// This generates a secret 32 characters in length. Do not modify the number of
|
||||
// bytes without updating the sanity checks in the enable and login endpoints.
|
||||
const secret = authenticator.generateSecret(20);
|
||||
|
||||
// generate backup codes with 10 character length
|
||||
// Generate backup codes with 10 character length
|
||||
const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));
|
||||
|
||||
await prisma.user.update({
|
||||
@@ -75,5 +78,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const keyUri = authenticator.keyuri(name, "Cal", secret);
|
||||
const dataUri = await qrcode.toDataURL(keyUri);
|
||||
|
||||
return res.json({ secret, keyUri, dataUri, backupCodes });
|
||||
return NextResponse.json({ secret, keyUri, dataUri, backupCodes });
|
||||
}
|
||||
+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,17 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { handleWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/handleWebhookScheduledTriggers";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
await handleWebhookScheduledTriggers(prisma);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
|
||||
|
||||
export { handler as POST };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
|
||||
|
||||
export { handler as POST };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
|
||||
|
||||
export { handler as POST };
|
||||
@@ -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,108 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
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 */,
|
||||
};
|
||||
};
|
||||
|
||||
async function handler(request: NextRequest) {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export default function EnterprisePage() {
|
||||
{
|
||||
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||
title: t("unified_billing"),
|
||||
description: t("unified_billing_description"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||
|
||||
@@ -120,6 +120,28 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
|
||||
const cardCharged = booking?.payment[0]?.success;
|
||||
|
||||
const attendeeList = booking.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
id: attendee.id,
|
||||
noShow: attendee.noShow || false,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
};
|
||||
});
|
||||
|
||||
const noShowMutation = trpc.viewer.markNoShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(data.message, "success");
|
||||
// Invalidate and refetch the bookings query to update the UI
|
||||
await utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === BookingStatus.REJECTED) {
|
||||
@@ -287,11 +309,22 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
if (isBookingInPast || isOngoing) {
|
||||
editBookingActions.push({
|
||||
id: "no_show",
|
||||
label: t("mark_as_no_show"),
|
||||
label:
|
||||
attendeeList.length === 1 && attendeeList[0].noShow ? t("unmark_as_no_show") : t("mark_as_no_show"),
|
||||
onClick: () => {
|
||||
// If there's only one attendee, mark them as no-show directly without showing the dialog
|
||||
if (attendeeList.length === 1) {
|
||||
const attendee = attendeeList[0];
|
||||
noShowMutation.mutate({
|
||||
bookingUid: booking.uid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsNoShowDialogOpen(true);
|
||||
},
|
||||
icon: "eye-off" as const,
|
||||
icon: attendeeList.length === 1 && attendeeList[0].noShow ? "eye" : ("eye-off" as const),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -427,15 +460,6 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
];
|
||||
|
||||
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
||||
const attendeeList = booking.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
id: attendee.id,
|
||||
noShow: attendee.noShow || false,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -14,11 +14,12 @@ import {
|
||||
LocationType,
|
||||
OrganizerDefaultConferencingAppType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import PhoneInput from "@calcom/features/components/phone-input";
|
||||
import type { LocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||
import LocationSelect from "@calcom/features/form/components/LocationSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "../../lib/QueryCell";
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
const ChangePasswordSection = () => {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
// hold display until the locale is loaded
|
||||
if (!isLocaleReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
|
||||
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
|
||||
};
|
||||
|
||||
async function changePasswordHandler(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/changepw", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ oldPassword, newPassword }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
setOldPassword("");
|
||||
setNewPassword("");
|
||||
showToast(t("password_has_been_changed"), "success");
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
|
||||
} catch (err) {
|
||||
console.error(t("error_changing_password"), err);
|
||||
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||
<div className="py-6 lg:pb-5">
|
||||
<div className="my-3">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
|
||||
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
|
||||
<label htmlFor="current_password" className="text-default block text-sm font-medium">
|
||||
{t("current_password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onInput={(e) => setOldPassword(e.currentTarget.value)}
|
||||
name="current_password"
|
||||
id="current_password"
|
||||
required
|
||||
className="border-default block w-full rounded-sm text-sm"
|
||||
placeholder={t("your_old_password")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sm:w-1/2">
|
||||
<label htmlFor="new_password" className="text-default block text-sm font-medium">
|
||||
{t("new_password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="new_password"
|
||||
id="new_password"
|
||||
value={newPassword}
|
||||
required
|
||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||
className="border-default block w-full rounded-sm text-sm"
|
||||
placeholder={t("super_secure_new_password")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
<div className="flex py-8 sm:justify-end">
|
||||
<Button color="secondary" type="submit">
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordSection;
|
||||
@@ -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,
|
||||
|
||||
@@ -175,7 +175,6 @@ export const config = {
|
||||
// Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here.
|
||||
// https://github.com/vercel/next.js/discussions/42458
|
||||
matcher: [
|
||||
"/",
|
||||
"/403",
|
||||
"/500",
|
||||
"/icons",
|
||||
|
||||
@@ -128,8 +128,10 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
userIds,
|
||||
attendeeName,
|
||||
attendeeEmail,
|
||||
afterStartDate: dateRange?.startDate ?? undefined,
|
||||
beforeEndDate: dateRange?.endDate ?? undefined,
|
||||
afterStartDate: dateRange?.startDate
|
||||
? dayjs(dateRange?.startDate).startOf("day").toISOString()
|
||||
: undefined,
|
||||
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -389,8 +391,9 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
variant="compact"
|
||||
ToolbarLeft={
|
||||
<>
|
||||
<DataTableFilters.AddFilterButton table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
|
||||
<DataTableFilters.ClearFiltersButton />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -321,12 +321,12 @@ export default function Success(props: PageProps) {
|
||||
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
|
||||
const host = bookingInfo.user.name || bookingInfo.user.email;
|
||||
if (isHost) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
return t(`${titlePrefix}emailed_host_and_attendees${titleSuffix}`, {
|
||||
user: attendee,
|
||||
});
|
||||
}
|
||||
if (isAttendee) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
return t(`${titlePrefix}emailed_host_and_attendees${titleSuffix}`, {
|
||||
user: host,
|
||||
});
|
||||
}
|
||||
@@ -335,7 +335,7 @@ export default function Success(props: PageProps) {
|
||||
attendee,
|
||||
});
|
||||
}
|
||||
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||
return t(`emailed_host_and_attendees${titleSuffix}`);
|
||||
}
|
||||
|
||||
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
|
||||
|
||||
@@ -54,8 +54,9 @@ function InsightsPageContent() {
|
||||
className="flex flex-wrap items-center gap-2"
|
||||
data-testid={`insights-filters-${isAll}-${teamId}-${userId}`}>
|
||||
<OrgTeamsFilter />
|
||||
<DataTableFilters.AddFilterButton table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
|
||||
<div className="grow" />
|
||||
<Download />
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
DialogTrigger,
|
||||
Editor,
|
||||
Form,
|
||||
Icon,
|
||||
ImageUploader,
|
||||
Label,
|
||||
PasswordField,
|
||||
@@ -654,6 +655,9 @@ const ProfileForm = ({
|
||||
/>
|
||||
</div>
|
||||
{extraField}
|
||||
<p className="text-subtle mt-1 flex items-center gap-1 text-sm">
|
||||
<Icon name="info" /> {t("tip_username_plus")}
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.0.12",
|
||||
"version": "5.0.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req });
|
||||
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
password: true,
|
||||
identityProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
res.status(404).json({ message: "User not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
|
||||
}
|
||||
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
|
||||
const currentPassword = user.password?.hash;
|
||||
if (!currentPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
|
||||
if (!passwordsMatch) {
|
||||
return res.status(403).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
if (oldPassword === newPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.NewPasswordMatchesOld });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
await prisma.userPassword.upsert({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
create: {
|
||||
hash: hashedPassword,
|
||||
userId: user.id,
|
||||
},
|
||||
update: {
|
||||
hash: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({ message: "Password updated successfully" });
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const requiredScopes = ["READ_PROFILE"];
|
||||
|
||||
const account = await isAuthorized(req, requiredScopes);
|
||||
|
||||
if (!account) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
return res.status(201).json({ username: account.name });
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
// This is the callback endpoint for the OIDC provider
|
||||
// A team must set this endpoint in the OIDC provider's configuration
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).send("Method not allowed");
|
||||
}
|
||||
|
||||
const { code, state } = req.query as {
|
||||
code: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
try {
|
||||
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
|
||||
|
||||
if (!redirect_url) {
|
||||
throw new HttpError({
|
||||
message: "No redirect URL found",
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
return res.redirect(302, redirect_url);
|
||||
} catch (err) {
|
||||
const { message, statusCode = 500 } = err as HttpError;
|
||||
|
||||
return res.status(statusCode).send(message);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { OAuthReq } from "@boxyhq/saml-jackson";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).send("Method not allowed");
|
||||
}
|
||||
|
||||
try {
|
||||
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq);
|
||||
|
||||
return res.redirect(302, redirect_url as string);
|
||||
} catch (err) {
|
||||
const { message, statusCode = 500 } = err as HttpError;
|
||||
|
||||
return res.status(statusCode).send(message);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
const { redirect_url } = await oauthController.samlResponse(req.body);
|
||||
|
||||
if (redirect_url) {
|
||||
res.redirect(302, redirect_url);
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
return await oauthController.token(req.body);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
|
||||
const extractAuthToken = (req: NextApiRequest) => {
|
||||
const authHeader = req.headers["authorization"];
|
||||
const parts = (authHeader || "").split(" ");
|
||||
if (parts.length > 1) return parts[1];
|
||||
|
||||
// check for query param
|
||||
let arr: string[] = [];
|
||||
const { access_token } = requestQuery.parse(req.query);
|
||||
arr = arr.concat(access_token);
|
||||
if (arr[0].length > 0) return arr[0];
|
||||
|
||||
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
};
|
||||
|
||||
const requestQuery = z.object({
|
||||
access_token: z.string(),
|
||||
});
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { oauthController } = await jackson();
|
||||
const token = extractAuthToken(req);
|
||||
return await oauthController.userInfo(token);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
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" });
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!session.user?.id) {
|
||||
console.error("Session is missing a user id.");
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
return res.status(400).json({ error: ErrorCode.TwoFactorSetupRequired });
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error("Missing encryption key; cannot proceed with two factor setup.");
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
if (secret.length !== 32) {
|
||||
console.error(
|
||||
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
|
||||
);
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||
if (!isValidToken) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.json({ message: "Two-factor enabled" });
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
export { default } from "@calcom/features/webhooks/lib/cron";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user