chore: auth on api-v2 with api-key (#15455)
* chore: enable apiv2 auth with api keys * chore: enable apiv2 auth with api keys * chore: enable apiv2 auth with api keys * chore: enable apiv2 auth with api keys * chore: enable apiv2 auth with api keys * fixup! chore: enable apiv2 auth with api keys * fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key * fixup! fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key * fixup! fixup! fixup! Merge branch 'chore-apiv2-auth-api-key' of github.com:calcom/cal.com into chore-apiv2-auth-api-key
This commit is contained in:
@@ -20,3 +20,7 @@ STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
WEB_APP_URL=http://localhost:3000/
|
||||
CALCOM_LICENSE_KEY=
|
||||
API_KEY_PREFIX=cal_
|
||||
GET_LICENSE_KEY_URL="https://console.cal.com/api/license"
|
||||
IS_E2E=false
|
||||
@@ -42,6 +42,16 @@ $ yarn prisma generate
|
||||
|
||||
Copy `.env.example` to `.env` and fill values.
|
||||
|
||||
## Add license Key to deployments table in DB
|
||||
|
||||
id, logo theme licenseKey agreedLicenseAt
|
||||
1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611'
|
||||
|
||||
your CALCOM_LICENSE_KEY env var need to contain the same value
|
||||
|
||||
.env
|
||||
CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
|
||||
@@ -30,9 +30,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
type: VersioningType.CUSTOM,
|
||||
extractor: (request: unknown) => {
|
||||
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
|
||||
console.log("asap header headerVersion", headerVersion);
|
||||
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
|
||||
console.log("asap return header headerVersion", headerVersion);
|
||||
return headerVersion;
|
||||
}
|
||||
return VERSION_2024_04_15;
|
||||
|
||||
@@ -15,6 +15,9 @@ const loadConfig = (): AppConfig => {
|
||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||
: ""
|
||||
}/v2`,
|
||||
keyPrefix: getEnv("API_KEY_PREFIX", "cal_"),
|
||||
licenseKey: getEnv("CALCOM_LICENSE_KEY", ""),
|
||||
licenceKeyUrl: getEnv("GET_LICENSE_KEY_URL", "https://console.cal.com/api/license"),
|
||||
},
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
@@ -31,6 +34,7 @@ const loadConfig = (): AppConfig => {
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
},
|
||||
e2e: getEnv("IS_E2E", false),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ export type AppConfig = {
|
||||
port: number;
|
||||
path: string;
|
||||
url: string;
|
||||
keyPrefix: string;
|
||||
licenseKey: string;
|
||||
licenceKeyUrl: string;
|
||||
};
|
||||
db: {
|
||||
readUrl: string;
|
||||
@@ -22,4 +25,5 @@ export type AppConfig = {
|
||||
app: {
|
||||
baseUrl: string;
|
||||
};
|
||||
e2e: boolean;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
|
||||
@@ -39,7 +39,7 @@ describe("Bookings Endpoints", () => {
|
||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
@@ -89,7 +89,7 @@ export class BookingsController {
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Permissions([BOOKING_READ])
|
||||
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
|
||||
@ApiQuery({ name: "limit", type: "number", required: false })
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ export class CalendarsController {
|
||||
private readonly googleCalendarService: GoogleCalendarService
|
||||
) {}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Get("/busy-times")
|
||||
async getBusyTimes(
|
||||
@Query() queryParams: CalendarBusyTimesInput,
|
||||
@@ -71,7 +71,7 @@ export class CalendarsController {
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
||||
const calendars = await this.calendarsService.getCalendars(userId);
|
||||
|
||||
@@ -81,7 +81,7 @@ export class CalendarsController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Get("/:calendar/connect")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async redirect(
|
||||
@@ -135,7 +135,7 @@ export class CalendarsController {
|
||||
|
||||
@Get("/:calendar/check")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@Permissions([APPS_READ])
|
||||
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
|
||||
switch (calendar) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
@@ -79,7 +79,7 @@ describe("Event types Endpoints", () => {
|
||||
let user: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
@@ -288,7 +288,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/:username/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
@@ -323,7 +323,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/:username/:eventSlug/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
@@ -340,7 +340,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
@@ -359,7 +359,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/public/:username/`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
@@ -375,7 +375,7 @@ describe("Event types Endpoints", () => {
|
||||
it(`/GET/:id not existing`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/1000`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
@@ -54,7 +54,7 @@ export class EventTypesController {
|
||||
|
||||
@Post("/")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async createEventType(
|
||||
@Body() body: CreateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
@@ -69,7 +69,7 @@ export class EventTypesController {
|
||||
|
||||
@Get("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async getEventType(
|
||||
@Param() params: EventTypeIdParams,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@@ -90,7 +90,7 @@ export class EventTypesController {
|
||||
|
||||
@Get("/")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||
const eventTypes = await getEventTypesByViewer({
|
||||
id: user.id,
|
||||
@@ -146,7 +146,7 @@ export class EventTypesController {
|
||||
|
||||
@Patch("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateEventType(
|
||||
@Param() params: EventTypeIdParams,
|
||||
@@ -164,7 +164,7 @@ export class EventTypesController {
|
||||
|
||||
@Delete("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async deleteEventType(
|
||||
@Param() params: EventTypeIdParams,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
@@ -61,7 +61,7 @@ export class GcalController {
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async redirect(
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Req() req: Request
|
||||
@@ -90,7 +90,7 @@ export class GcalController {
|
||||
|
||||
@Get("/check")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@Permissions([APPS_READ])
|
||||
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
|
||||
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { UserResponse } from "@calcom/platform-types";
|
||||
@@ -30,7 +30,7 @@ describe("Me Endpoints", () => {
|
||||
let user: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
@@ -18,7 +18,7 @@ import { userSchemaResponse } from "@calcom/platform-types";
|
||||
path: "/v2/me",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@DocsTags("Me")
|
||||
export class MeController {
|
||||
constructor(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-ac
|
||||
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ export class CalProviderController {
|
||||
|
||||
@Get("/:clientId/access-token")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async verifyAccessToken(
|
||||
@Param("clientId") clientId: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
|
||||
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||
@@ -37,7 +37,7 @@ describe("Schedules Endpoints", () => {
|
||||
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
|
||||
import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input"
|
||||
path: "/v2/schedules",
|
||||
version: VERSION_2024_04_15_VALUE,
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@DocsTags("Schedules")
|
||||
export class SchedulesController_2024_04_15 {
|
||||
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
||||
import {
|
||||
@@ -52,7 +52,7 @@ describe("Schedules Endpoints", () => {
|
||||
let createdSchedule: CreateScheduleOutput_2024_06_11["data"];
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_06_11],
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11
|
||||
import { VERSION_2024_06_11_VALUE } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
path: "/v2/schedules",
|
||||
version: VERSION_2024_06_11_VALUE,
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@DocsTags("Schedules")
|
||||
export class SchedulesController_2024_06_11 {
|
||||
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
||||
|
||||
@@ -15,6 +15,10 @@ export type Environment = {
|
||||
STRIPE_API_KEY: string;
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
WEB_APP_URL: string;
|
||||
IS_E2E: boolean;
|
||||
CALCOM_LICENSE_KEY: string;
|
||||
GET_LICENSE_KEY_URL: string;
|
||||
API_KEY_PREFIX: string;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
|
||||
|
||||
export const isApiKey = (authString: string, prefix: string): boolean =>
|
||||
authString?.startsWith(prefix ?? "cal_");
|
||||
|
||||
export const stripApiKey = (apiKey: string, prefix?: string): string => apiKey.replace(prefix ?? "cal_", "");
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { VersionValue } from "@nestjs/common/interfaces";
|
||||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
VERSION_2024_04_15,
|
||||
VERSION_2024_05_21,
|
||||
VERSION_2024_06_11,
|
||||
} from "@calcom/platform-constants";
|
||||
import { API_VERSIONS, VERSION_2024_04_15, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
||||
|
||||
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
||||
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
||||
export const VERSION_2024_05_21_VALUE: VersionValue = VERSION_2024_05_21 as unknown as VersionValue;
|
||||
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getApiKeyFromHash(hashedKey: string) {
|
||||
return this.dbRead.prisma.apiKey.findUnique({
|
||||
where: {
|
||||
hashedKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ApiKeyService],
|
||||
exports: [ApiKeyService],
|
||||
providers: [ApiKeyRepository],
|
||||
exports: [ApiKeyRepository],
|
||||
})
|
||||
export class ApiKeyModule {}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { hashAPIKey } from "@/lib/api-key";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
|
||||
async retrieveApiKey(request: Request) {
|
||||
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
|
||||
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashedKey = hashAPIKey(apiKey.replace("cal_", ""));
|
||||
|
||||
return this.dbRead.prisma.apiKey.findUniqueOrThrow({
|
||||
where: {
|
||||
hashedKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { AccessTokenStrategy } from "@/modules/auth/strategies/access-token/access-token.strategy";
|
||||
import { ApiKeyAuthStrategy } from "@/modules/auth/strategies/api-key-auth/api-key-auth.strategy";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
||||
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
@@ -13,15 +13,16 @@ import { Module } from "@nestjs/common";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule, RedisModule, ApiKeyModule, UsersModule, MembershipsModule, TokensModule],
|
||||
providers: [
|
||||
ApiKeyAuthStrategy,
|
||||
NextAuthGuard,
|
||||
NextAuthStrategy,
|
||||
AccessTokenGuard,
|
||||
AccessTokenStrategy,
|
||||
OAuthFlowService,
|
||||
imports: [
|
||||
PassportModule,
|
||||
RedisModule,
|
||||
ApiKeyModule,
|
||||
UsersModule,
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
DeploymentsModule,
|
||||
],
|
||||
exports: [NextAuthGuard, AccessTokenGuard],
|
||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||
exports: [NextAuthGuard, ApiAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
export const GetUser = createParamDecorator<keyof User | (keyof User)[], ExecutionContext>((data, ctx) => {
|
||||
export type GetUserReturnType = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
export const GetUser = createParamDecorator<
|
||||
keyof GetUserReturnType | (keyof GetUserReturnType)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user as User;
|
||||
const user = request.user as GetUserReturnType;
|
||||
|
||||
if (!user) {
|
||||
throw new Error("GetUser decorator : User not found");
|
||||
}
|
||||
|
||||
user.isSystemAdmin = user.role === "ADMIN";
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
...prev,
|
||||
[curr]: request.user[curr],
|
||||
[curr]: user[curr],
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return request.user[data];
|
||||
return user[data];
|
||||
}
|
||||
|
||||
return user;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
export class AccessTokenGuard extends AuthGuard("access-token") {
|
||||
export class ApiAuthGuard extends AuthGuard("api-auth") {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
import { APPS_WRITE, SCHEDULE_READ, SCHEDULE_WRITE } from "@calcom/platform-constants";
|
||||
@@ -13,7 +14,7 @@ describe("PermissionsGuard", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
reflector = new Reflector();
|
||||
guard = new PermissionsGuard(reflector, createMock<TokensRepository>());
|
||||
guard = new PermissionsGuard(reflector, createMock<TokensRepository>(), createMock<ConfigService>());
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { isApiKey } from "@/lib/api-key";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
import { hasPermissions } from "@calcom/platform-utils";
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private tokensRepository: TokensRepository) {}
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private tokensRepository: TokensRepository,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requiredPermissions = this.reflector.get(Permissions, context.getHandler());
|
||||
@@ -17,13 +23,18 @@ export class PermissionsGuard implements CanActivate {
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
||||
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||
|
||||
if (!accessToken) {
|
||||
if (!authString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oAuthClientPermissions = await this.getOAuthClientPermissions(accessToken);
|
||||
// only check permissions for accessTokens attached to an oAuth Client
|
||||
if (isApiKey(authString, this.config.get("api.apiKeyPrefix") ?? "cal_")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oAuthClientPermissions = await this.getOAuthClientPermissions(authString);
|
||||
|
||||
if (!oAuthClientPermissions) {
|
||||
return false;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenStrategy extends PassportStrategy(BaseStrategy, "access-token") {
|
||||
constructor(
|
||||
private readonly oauthFlowService: OAuthFlowService,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly userRepository: UsersRepository
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request) {
|
||||
try {
|
||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
||||
const requestOrigin = request.get("Origin");
|
||||
|
||||
if (!accessToken) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||
if (!accessTokenValid) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
const client = await this.tokensRepository.getAccessTokenClient(accessToken);
|
||||
if (!client) {
|
||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
||||
}
|
||||
|
||||
if (requestOrigin && !client.redirectUris.some((uri) => uri.startsWith(requestOrigin))) {
|
||||
throw new UnauthorizedException(
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${requestOrigin}' to the 'Redirect uris' of your OAuth client.`
|
||||
);
|
||||
}
|
||||
|
||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||
|
||||
if (!ownerId) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
return this.success(user);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return this.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import appConfig from "@/config/app";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { MockedRedisService } from "test/mocks/mock-redis-service";
|
||||
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
let strategy: ApiAuthStrategy;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let organization: Team;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
const validApiKeyEmail = "api-key-user-email@example.com";
|
||||
const validAccessTokenEmail = "access-token-user-email@example.com";
|
||||
let validApiKeyUser: User;
|
||||
let validAccessTokenUser: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvFile: true,
|
||||
isGlobal: true,
|
||||
load: [appConfig],
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
MockedRedisService,
|
||||
ApiAuthStrategy,
|
||||
ConfigService,
|
||||
OAuthFlowService,
|
||||
UsersRepository,
|
||||
ApiKeyRepository,
|
||||
DeploymentsService,
|
||||
OAuthClientRepository,
|
||||
PrismaReadService,
|
||||
PrismaWriteService,
|
||||
TokensRepository,
|
||||
JwtService,
|
||||
DeploymentsRepository,
|
||||
NestJwtService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
strategy = module.get<ApiAuthStrategy>(ApiAuthStrategy);
|
||||
userRepositoryFixture = new UserRepositoryFixture(module);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(module);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(module);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
validApiKeyUser = await userRepositoryFixture.create({
|
||||
email: validApiKeyEmail,
|
||||
});
|
||||
validAccessTokenUser = await userRepositoryFixture.create({
|
||||
email: validAccessTokenEmail,
|
||||
});
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:3000"],
|
||||
permissions: 32,
|
||||
};
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||
});
|
||||
|
||||
describe("authenticate with strategy", () => {
|
||||
it("should return user associated with valid access token", async () => {
|
||||
console.log("HERERERER");
|
||||
const { accessToken } = await tokensRepositoryFixture.createTokens(
|
||||
validAccessTokenUser.id,
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
console.log("Access token", accessToken);
|
||||
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer ${accessToken}`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const user = await strategy.authenticate(request);
|
||||
|
||||
await expect(user.id).toEqual(validAccessTokenUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: `Bearer cal_test_${keyString}`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer cal_test_${keyString}`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const user = await strategy.authenticate(request);
|
||||
|
||||
expect(user.id).toEqual(validApiKeyUser.id);
|
||||
});
|
||||
|
||||
it("should throw 401 if api key is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: `Bearer cal_test_}`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if Authorization header does not contain auth token", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
get: (key: string) => ({ Authorization: ``, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
constructor(
|
||||
private readonly deploymentsService: DeploymentsService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly oauthFlowService: OAuthFlowService,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly apiKeyRepository: ApiKeyRepository
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request) {
|
||||
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!authString) {
|
||||
return this.error(new UnauthorizedException("No Authorization header provided"));
|
||||
}
|
||||
|
||||
const requestOrigin = request.get("Origin");
|
||||
|
||||
try {
|
||||
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? await this.apiKeyStrategy(authString)
|
||||
: await this.accessTokenStrategy(authString, requestOrigin);
|
||||
|
||||
if (!user) {
|
||||
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||
}
|
||||
|
||||
return this.success(user);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return this.error(err);
|
||||
}
|
||||
return this.error(
|
||||
new InternalServerErrorException("An error occurred while authenticating the request")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async apiKeyStrategy(apiKey: string) {
|
||||
const isLicenseValid = await this.deploymentsService.checkLicense();
|
||||
if (!isLicenseValid) {
|
||||
throw new UnauthorizedException("Invalid or missing CALCOM_LICENSE_KEY environment variable");
|
||||
}
|
||||
const strippedApiKey = stripApiKey(apiKey, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = hashAPIKey(strippedApiKey);
|
||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||
if (!keyData) {
|
||||
throw new UnauthorizedException("Your api key is not valid");
|
||||
}
|
||||
|
||||
const isKeyExpired =
|
||||
keyData.expiresAt && new Date().setHours(0, 0, 0, 0) > keyData.expiresAt.setHours(0, 0, 0, 0);
|
||||
if (isKeyExpired) {
|
||||
throw new UnauthorizedException("Your api key is expired");
|
||||
}
|
||||
|
||||
const apiKeyOwnerId = keyData.userId;
|
||||
if (!apiKeyOwnerId) {
|
||||
throw new UnauthorizedException("No user tied to this apiKey");
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(apiKeyOwnerId);
|
||||
return user;
|
||||
}
|
||||
|
||||
async accessTokenStrategy(accessToken: string, origin?: string) {
|
||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||
if (!accessTokenValid) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
const client = await this.tokensRepository.getAccessTokenClient(accessToken);
|
||||
if (!client) {
|
||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
||||
}
|
||||
|
||||
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
|
||||
throw new UnauthorizedException(
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
|
||||
);
|
||||
}
|
||||
|
||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||
|
||||
if (!ownerId) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthStrategy extends PassportStrategy(BaseStrategy, "api-key") {
|
||||
constructor(
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly userRepository: UsersRepository
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(req: Request) {
|
||||
try {
|
||||
const apiKey = await this.apiKeyService.retrieveApiKey(req);
|
||||
|
||||
if (!apiKey) {
|
||||
throw new UnauthorizedException("Authorization token is missing.");
|
||||
}
|
||||
|
||||
if (apiKey.expiresAt && new Date() > apiKey.expiresAt) {
|
||||
throw new UnauthorizedException("The API key is expired.");
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(apiKey.userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
|
||||
this.success(user);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return this.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
@@ -33,6 +33,9 @@ export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy,
|
||||
return this.success(user);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return this.error(error);
|
||||
return this.error(
|
||||
new InternalServerErrorException("An error occurred while authenticating the request")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { DeploymentsRepository } from "./deployments.repository";
|
||||
import { DeploymentsService } from "./deployments.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [DeploymentsRepository, DeploymentsService, RedisService],
|
||||
exports: [DeploymentsRepository, DeploymentsService],
|
||||
})
|
||||
export class DeploymentsModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class DeploymentsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getDeployment() {
|
||||
return this.dbRead.prisma.deployment.findFirst({ where: { id: 1 } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
const CACHING_TIME = 86400000; // 24 hours in milliseconds
|
||||
|
||||
const getLicenseCacheKey = (key: string) => `api-v2-license-key-url-${key}`;
|
||||
|
||||
type LicenseCheckResponse = {
|
||||
valid: boolean;
|
||||
};
|
||||
@Injectable()
|
||||
export class DeploymentsService {
|
||||
constructor(
|
||||
private readonly deploymentsRepository: DeploymentsRepository,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly redisService: RedisService
|
||||
) {}
|
||||
|
||||
async checkLicense() {
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
let licenseKey = this.configService.get("api.licenseKey");
|
||||
|
||||
if (!licenseKey) {
|
||||
/** We try to check on DB only if env is undefined */
|
||||
const deployment = await this.deploymentsRepository.getDeployment();
|
||||
licenseKey = deployment?.licenseKey ?? undefined;
|
||||
}
|
||||
|
||||
if (!licenseKey) {
|
||||
return false;
|
||||
}
|
||||
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl");
|
||||
const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey));
|
||||
if (cachedData) {
|
||||
return (JSON.parse(cachedData) as LicenseCheckResponse)?.valid;
|
||||
}
|
||||
const response = await fetch(licenseKeyUrl, { mode: "cors" });
|
||||
const data = (await response.json()) as LicenseCheckResponse;
|
||||
const cacheKey = getLicenseCacheKey(licenseKey);
|
||||
this.redisService.redis.set(cacheKey, JSON.stringify(data), "EX", CACHING_TIME);
|
||||
return data.valid;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
||||
import { TokenExpiredException } from "@/modules/auth/guards/api-auth/token-expired.exception";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TimeFormat, WeekDay } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNumber, IsOptional, IsString, IsTimeZone } from "class-validator";
|
||||
import { IsIn, IsNumber, IsOptional, IsString, IsTimeZone } from "class-validator";
|
||||
|
||||
export class UpdateManagedUserInput {
|
||||
@IsString()
|
||||
@@ -12,6 +12,7 @@ export class UpdateManagedUserInput {
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(["12", "24"])
|
||||
@ApiProperty({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
||||
timeFormat?: TimeFormat;
|
||||
|
||||
@@ -21,6 +22,7 @@ export class UpdateManagedUserInput {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
||||
@ApiProperty({
|
||||
example: "Monday",
|
||||
enum: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
||||
|
||||
@@ -810,16 +810,6 @@
|
||||
"post": {
|
||||
"operationId": "SchedulesController_2024_06_11_createSchedule",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
@@ -862,14 +852,7 @@
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns the default schedule",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
"description": "Returns the default schedule"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
@@ -918,16 +901,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -1890,16 +1863,15 @@
|
||||
"properties": {
|
||||
"timeFormat": {
|
||||
"type": "number",
|
||||
"example": 12,
|
||||
"enum": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"example": 12,
|
||||
"description": "Must be 12 or 24"
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string",
|
||||
"example": "Monday",
|
||||
"enum": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
@@ -1908,7 +1880,8 @@
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday"
|
||||
]
|
||||
],
|
||||
"example": "Monday"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
@@ -1934,11 +1907,15 @@
|
||||
"refreshToken": {
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
},
|
||||
"accessTokenExpiresAt": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken",
|
||||
"refreshToken"
|
||||
"refreshToken",
|
||||
"accessTokenExpiresAt"
|
||||
]
|
||||
},
|
||||
"KeysResponseDto": {
|
||||
@@ -3303,66 +3280,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"schedule": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dateOverrides": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeZone",
|
||||
"name",
|
||||
"isDefault",
|
||||
"schedule"
|
||||
]
|
||||
},
|
||||
"EventTypeModel_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3618,15 +3535,17 @@
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"pattern": "TIME_FORMAT_HH_MM",
|
||||
"example": "09:00"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"pattern": "TIME_FORMAT_HH_MM",
|
||||
"example": "10:00"
|
||||
}
|
||||
},
|
||||
@@ -3645,10 +3564,12 @@
|
||||
},
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"pattern": "TIME_FORMAT_HH_MM",
|
||||
"example": "12:00"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"pattern": "TIME_FORMAT_HH_MM",
|
||||
"example": "13:00"
|
||||
}
|
||||
},
|
||||
@@ -3658,57 +3579,6 @@
|
||||
"endTime"
|
||||
]
|
||||
},
|
||||
"CreateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"timeZone",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
"ScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3792,26 +3662,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"GetDefaultScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"GetScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3866,52 +3716,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { randomBytes, createHash } from "crypto";
|
||||
|
||||
export class ApiKeysRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async createApiKey(userId: number, expiresAt: Date | null, teamId?: number) {
|
||||
const keyString = randomBytes(16).toString("hex");
|
||||
const apiKey = await this.prismaWriteClient.apiKey.create({
|
||||
data: {
|
||||
userId,
|
||||
teamId,
|
||||
hashedKey: createHash("sha256").update(keyString).digest("hex"),
|
||||
expiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
return { apiKey, keyString };
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenMockStrategy extends PassportStrategy(BaseStrategy, "access-token") {
|
||||
export class ApiAuthMockStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
constructor(private readonly email: string, private readonly usersRepository: UsersRepository) {
|
||||
super();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Provider } from "@nestjs/common";
|
||||
|
||||
export const MockedRedisService = {
|
||||
provide: RedisService,
|
||||
useValue: {
|
||||
redis: {
|
||||
get: jest.fn(),
|
||||
hgetall: jest.fn(),
|
||||
set: jest.fn(),
|
||||
hmset: jest.fn(),
|
||||
expireat: jest.fn(),
|
||||
},
|
||||
},
|
||||
} as Provider;
|
||||
@@ -1,15 +1,21 @@
|
||||
import type { Environment } from "@/env";
|
||||
|
||||
const env: Partial<Omit<Environment, "NODE_ENV">> = {
|
||||
API_URL: "http://localhost",
|
||||
API_PORT: "5555",
|
||||
DATABASE_URL: "postgresql://postgres:@localhost:5450/calendso",
|
||||
DATABASE_READ_URL: "postgresql://postgres:@localhost:5450/calendso",
|
||||
DATABASE_WRITE_URL: "postgresql://postgres:@localhost:5450/calendso",
|
||||
NEXTAUTH_SECRET: "XF+Hws3A5g2eyWA5uGYYVJ74X+wrCWJ8oWo6kAfU6O8=",
|
||||
JWT_SECRET: "XF+Hws3A5g2eyWA5uGYYVJ74X+wrCWJ8oWo6kAfU6O8=",
|
||||
LOG_LEVEL: "trace",
|
||||
REDIS_URL: "redis://localhost:9199",
|
||||
REDIS_URL: "redis://localhost:6379",
|
||||
STRIPE_API_KEY: "sk_test_51J4",
|
||||
STRIPE_WEBHOOK_SECRET: "whsec_51J4",
|
||||
IS_E2E: true,
|
||||
API_KEY_PREFIX: "cal_test_",
|
||||
GET_LICENSE_KEY_URL: " https://console.cal.com/api/license",
|
||||
CALCOM_LICENSE_KEY: "c4234812-12ab-42s6-a1e3-55bedd4a5bb7",
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { AccessTokenStrategy } from "@/modules/auth/strategies/access-token/access-token.strategy";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { TestingModuleBuilder } from "@nestjs/testing";
|
||||
import { AccessTokenMockStrategy } from "test/mocks/access-token-mock.strategy";
|
||||
|
||||
export const withAccessTokenAuth = (email: string, module: TestingModuleBuilder) =>
|
||||
module.overrideProvider(AccessTokenStrategy).useFactory({
|
||||
factory: (usersRepository: UsersRepository) => new AccessTokenMockStrategy(email, usersRepository),
|
||||
inject: [UsersRepository],
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { TestingModuleBuilder } from "@nestjs/testing";
|
||||
import { ApiAuthMockStrategy } from "test/mocks/api-auth-mock.strategy";
|
||||
|
||||
export const withApiAuth = (email: string, module: TestingModuleBuilder) =>
|
||||
module.overrideProvider(ApiAuthStrategy).useFactory({
|
||||
factory: (usersRepository: UsersRepository) => new ApiAuthMockStrategy(email, usersRepository),
|
||||
inject: [UsersRepository],
|
||||
});
|
||||
@@ -52,9 +52,8 @@ export const X_CAL_CLIENT_ID = "x-cal-client-id";
|
||||
export const HTTP_CODE_TOKEN_EXPIRED = 498;
|
||||
|
||||
export const VERSION_2024_06_11 = "2024-06-11";
|
||||
export const VERSION_2024_05_21 = "2024-05-21";
|
||||
export const VERSION_2024_04_15 = "2024-04-15";
|
||||
export const API_VERSIONS = [VERSION_2024_06_11, VERSION_2024_04_15, VERSION_2024_05_21] as const;
|
||||
export const API_VERSIONS = [VERSION_2024_06_11, VERSION_2024_04_15] as const;
|
||||
export type API_VERSIONS_ENUM = (typeof API_VERSIONS)[number];
|
||||
export type API_VERSIONS_TYPE = typeof API_VERSIONS;
|
||||
export const CAL_API_VERSION_HEADER = "cal-api-version";
|
||||
|
||||
Reference in New Issue
Block a user