feat: v2 managed organizations (#19341)
* refactor: allow non unique PlatformBilling customerId * feat: add PlatformBilling managed organizations fields * feat: ManagedOrganization table * refactor: bill overdue based on teamId * refactor: restructure organizations module * wip: organizations endpoints * Revert "wip: organizations endpoints" This reverts commit 0e9e66fc74f31da436f12930c1b6597c650ed621. * refactor: unique index for managed organizations * wip: organizations endpoints * wip: create managed organization * remove unecessary membership check because we have guard * feat: create managed org * feat: get managed org and orgs * feat: update and delete managed orgs * wip: api key logic * feat: allow variable api key length * feat: refresh managed org api key * feat: create managed org OAuth clients using api key * finish merge main * chore: bump platform libraries * tests: fix and add more tests to api-auth.strategy.e2e * refactor: dont request managed org slug and handle metadata as object * revert: billing service and repository update overdue based on sub and customer ids * refactor: v2 OAuth client permissions (#19501) * refactor: v2 permissions as string array * refactor: frontend work with permissions as string * tests: test '*' permissions * fix:managed org creator have profile & test can fetch oauth client * fix: tests * fix: OAuthClientCard on frontend * fix: tests
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.99",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.110",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -91,6 +91,7 @@
|
||||
"@types/supertest": "^2.0.12",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"prettier": "^2.8.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -11,7 +11,7 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeyRepository],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
export class BookingsModule_2024_04_15 {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/g
|
||||
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -98,7 +98,7 @@ export class BookingsController_2024_04_15 {
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository
|
||||
private readonly apiKeyRepository: ApiKeysRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
@@ -29,7 +29,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingsRepository_2024_08_13,
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
ApiKeyRepository,
|
||||
ApiKeysRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -74,7 +74,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository
|
||||
) {}
|
||||
|
||||
|
||||
@@ -3,12 +3,7 @@ import { ZodSchema } from "zod";
|
||||
|
||||
const logger = new Logger("safeParse");
|
||||
|
||||
export function safeParse<T>(
|
||||
schema: ZodSchema<T>,
|
||||
value: unknown,
|
||||
defaultValue: T,
|
||||
logError = true
|
||||
): T {
|
||||
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T, logError = true): T {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
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: [ApiKeyRepository],
|
||||
exports: [ApiKeyRepository],
|
||||
})
|
||||
export class ApiKeyModule {}
|
||||
+17
-1
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository {
|
||||
export class ApiKeysRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getApiKeyFromHash(hashedKey: string) {
|
||||
@@ -13,4 +13,20 @@ export class ApiKeyRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamApiKeys(teamId: number) {
|
||||
return this.dbRead.prisma.apiKey.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
return this.dbWrite.prisma.apiKey.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { ApiKeysController } from "@/modules/api-keys/controllers/api-keys.controller";
|
||||
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ApiKeysRepository, ApiKeysService],
|
||||
controllers: [ApiKeysController],
|
||||
exports: [ApiKeysRepository, ApiKeysService],
|
||||
})
|
||||
export class ApiKeysModule {}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output";
|
||||
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { ApiAuthGuardRequest } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from "@nestjs/common";
|
||||
import { ApiTags, ApiHeader, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/api-keys",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiTags("Api Keys")
|
||||
export class ApiKeysController {
|
||||
constructor(private readonly apiKeysService: ApiKeysService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Refresh API Key",
|
||||
description: `Generate a new API key and delete the current one. Provide API key to refresh as a Bearer token in the Authorization header (e.g. "Authorization: Bearer <apiKey>").`,
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description: "Bearer <apiKey>",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
})
|
||||
@Post("/refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async refresh(
|
||||
@Body() body: RefreshApiKeyInput,
|
||||
@GetUser("id") userId: number,
|
||||
@Req() request: ApiAuthGuardRequest
|
||||
): Promise<RefreshApiKeyOutput> {
|
||||
const currentApiKey = await this.apiKeysService.getRequestApiKey(request);
|
||||
const newApiKey = await this.apiKeysService.refreshApiKey(userId, currentApiKey, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
apiKey: newApiKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateApiKeyInput extends RefreshApiKeyInput {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({
|
||||
description: "For which team or organization is the api key.",
|
||||
})
|
||||
readonly teamId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: "Store additional note about this api key.",
|
||||
example: "This is an api key for development purposes.",
|
||||
})
|
||||
readonly note?: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsInt, IsOptional, Min } from "class-validator";
|
||||
|
||||
export class RefreshApiKeyInput {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "For how many days is managed organization api key valid. Defaults to 30 days.",
|
||||
example: 60,
|
||||
default: 30,
|
||||
minimum: 1,
|
||||
})
|
||||
readonly apiKeyDaysValid?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ description: "If true, organization api key never expires.", example: true })
|
||||
readonly apiKeyNeverExpires?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class ApiKeyOutput {
|
||||
@IsString()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly apiKey!: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiKeyOutput } from "@/modules/api-keys/outputs/api-key.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class RefreshApiKeyOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({ type: ApiKeyOutput })
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ApiKeyOutput)
|
||||
data!: ApiKeyOutput;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { hashAPIKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { CreateApiKeyInput } from "@/modules/api-keys/inputs/create-api-key.input";
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { ApiAuthGuardRequest } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { createApiKeyHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeysService {
|
||||
constructor(
|
||||
private readonly apiKeysRepository: ApiKeysRepository,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
async getRequestApiKey(request: ApiAuthGuardRequest) {
|
||||
if (request.authMethod !== AuthMethods.API_KEY) {
|
||||
throw new UnauthorizedException(
|
||||
"ApiKeysService - This endpoint can only be accessed using an API key by providing 'Authorization: Bearer <apiKey>' header"
|
||||
);
|
||||
}
|
||||
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!apiKey) {
|
||||
throw new UnauthorizedException("ApiKeysService - No API key provided");
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput) {
|
||||
if (createApiKeyInput.apiKeyDaysValid && createApiKeyInput.apiKeyNeverExpires) {
|
||||
throw new BadRequestException(
|
||||
"ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNeverExpires. It has to be either or none of them."
|
||||
);
|
||||
}
|
||||
|
||||
const defaultApiKeyDaysValid = 30;
|
||||
const apiKeyExpiresAfterDays = createApiKeyInput.apiKeyDaysValid
|
||||
? createApiKeyInput.apiKeyDaysValid
|
||||
: defaultApiKeyDaysValid;
|
||||
const apiKeyExpiresAt = DateTime.utc().plus({ days: apiKeyExpiresAfterDays }).toJSDate();
|
||||
const apiKey = await createApiKeyHandler({
|
||||
ctx: {
|
||||
user: {
|
||||
id: authUserId,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
note: createApiKeyInput.note,
|
||||
neverExpires: !!createApiKeyInput.apiKeyNeverExpires,
|
||||
expiresAt: apiKeyExpiresAt,
|
||||
teamId: createApiKeyInput.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
async refreshApiKey(authUserId: number, apiKey: string, refreshApiKeyInput: RefreshApiKeyInput) {
|
||||
const strippedApiKey = stripApiKey(apiKey, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = hashAPIKey(strippedApiKey);
|
||||
const apiKeyInDb = await this.apiKeysRepository.getApiKeyFromHash(apiKeyHash);
|
||||
if (!apiKeyInDb) {
|
||||
throw new UnauthorizedException("ApiKeysService - provided api key is not valid.");
|
||||
}
|
||||
|
||||
const newApiKey = await this.createApiKey(authUserId, {
|
||||
...refreshApiKeyInput,
|
||||
note: apiKeyInDb.note || undefined,
|
||||
teamId: apiKeyInDb.teamId || undefined,
|
||||
});
|
||||
|
||||
await this.apiKeysRepository.deleteById(apiKeyInDb.id);
|
||||
|
||||
return newApiKey;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||
import { ApiKeysModule } from "@/modules/api-keys/api-keys.module";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
@@ -17,7 +17,7 @@ import { PassportModule } from "@nestjs/passport";
|
||||
imports: [
|
||||
PassportModule,
|
||||
RedisModule,
|
||||
ApiKeyModule,
|
||||
ApiKeysModule,
|
||||
UsersModule,
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createParamDecorator, ExecutionContext, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
export const GetOrgId = createParamDecorator((_: unknown, ctx: ExecutionContext): number => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const organizationId = request.organizationId;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException("GetOrgId decorator: Organization ID not found");
|
||||
}
|
||||
|
||||
return organizationId;
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
@@ -20,10 +20,10 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const organizationId = this.getOrganizationId(context);
|
||||
|
||||
if (!user || !organizationId) {
|
||||
throw new ForbiddenException("No organization associated with the user.");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - No organization associated with the user.");
|
||||
}
|
||||
|
||||
await this.isPlatform(organizationId);
|
||||
@@ -40,13 +40,25 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async isPlatform(organizationId: number) {
|
||||
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
||||
if (!isPlatform) {
|
||||
throw new ForbiddenException("Organization is not a platform (SHP).");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - Organization is not a platform (SHP).");
|
||||
}
|
||||
}
|
||||
|
||||
getOrganizationId(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const authMethodOrganizationId = request.organizationId;
|
||||
if (authMethodOrganizationId) return authMethodOrganizationId;
|
||||
|
||||
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
return userOrganizationId;
|
||||
}
|
||||
|
||||
isMembershipAccepted(accepted: boolean) {
|
||||
if (!accepted) {
|
||||
throw new ForbiddenException(`User has not accepted membership in the organization.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User has not accepted membership in the organization.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +69,9 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
|
||||
const hasRequiredRole = allowedRoles.includes(membershipRole);
|
||||
if (!hasRequiredRole) {
|
||||
throw new ForbiddenException(`User must have one of the roles: ${allowedRoles.join(", ")}.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User must have one of the roles: ${allowedRoles.join(", ")}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsManagedOrgInManagerOrg implements CanActivate {
|
||||
constructor(private managedOrganizationsRepository: ManagedOrganizationsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { managedOrganization: Team }>();
|
||||
const managedOrgId: string = request.params.managedOrganizationId;
|
||||
const managerOrgId: string = request.params.orgId;
|
||||
|
||||
if (!managerOrgId) {
|
||||
throw new ForbiddenException("No manager org id found in request params.");
|
||||
}
|
||||
|
||||
if (!managedOrgId) {
|
||||
throw new ForbiddenException("No managed org id found in request params.");
|
||||
}
|
||||
|
||||
const managedOrganization = await this.managedOrganizationsRepository.getByManagerManagedIds(
|
||||
Number(managerOrgId),
|
||||
Number(managedOrgId)
|
||||
);
|
||||
|
||||
if (!managedOrganization) {
|
||||
throw new NotFoundException(
|
||||
`Managed organization with id=${managedOrgId} within manager organization with id=${managerOrgId} not found.`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import appConfig from "@/config/app";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
@@ -9,6 +10,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -16,6 +18,7 @@ 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 { createRequest } from "node-mocks-http";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -27,6 +30,7 @@ import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { ApiAuthGuardRequest } from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -35,7 +39,9 @@ describe("ApiAuthStrategy", () => {
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let organization: Team;
|
||||
let organizationTwo: Team;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let oAuthClientTwo: PlatformOAuthClient;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
@@ -65,7 +71,8 @@ describe("ApiAuthStrategy", () => {
|
||||
ConfigService,
|
||||
OAuthFlowService,
|
||||
UsersRepository,
|
||||
ApiKeyRepository,
|
||||
UsersService,
|
||||
ApiKeysRepository,
|
||||
DeploymentsService,
|
||||
OAuthClientRepository,
|
||||
PrismaReadService,
|
||||
@@ -84,7 +91,11 @@ describe("ApiAuthStrategy", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-${randomString()}` });
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||
organizationTwo = await teamRepositoryFixture.create({
|
||||
name: `api-auth-organization-2-${randomString()}`,
|
||||
});
|
||||
|
||||
validApiKeyUser = await userRepositoryFixture.create({
|
||||
email: validApiKeyEmail,
|
||||
});
|
||||
@@ -103,6 +114,13 @@ describe("ApiAuthStrategy", () => {
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: validOAuthEmail,
|
||||
user: { connect: { id: validOAuthUser.id } },
|
||||
organization: { connect: { id: organizationTwo.id } },
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
@@ -110,6 +128,7 @@ describe("ApiAuthStrategy", () => {
|
||||
permissions: 32,
|
||||
};
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||
oAuthClientTwo = await oAuthClientRepositoryFixture.create(organizationTwo.id, data, "secret");
|
||||
});
|
||||
|
||||
describe("authenticate with strategy", () => {
|
||||
@@ -119,25 +138,68 @@ describe("ApiAuthStrategy", () => {
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const user = await strategy.accessTokenStrategy(accessToken);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.ACCESS_TOKEN;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.accessTokenStrategy(accessToken, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validAccessTokenUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key", async () => {
|
||||
it("should return user associated with valid api key and correctly set organizationId for org1", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organization.id
|
||||
);
|
||||
|
||||
const user = await strategy.apiKeyStrategy(keyString);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key and correctly set organizationId for org2", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organizationTwo.id
|
||||
);
|
||||
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client", async () => {
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client for org2", async () => {
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClientTwo.id, oAuthClientTwo.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should throw 401 if api key is invalid", async () => {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -18,7 +19,11 @@ import { getToken } from "next-auth/jwt";
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
export type ApiAuthGuardRequest = Request & {
|
||||
authMethod: AuthMethods;
|
||||
organizationId: number | null;
|
||||
user: ApiAuthGuardUser;
|
||||
};
|
||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||
@Injectable()
|
||||
@@ -29,14 +34,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
private readonly oauthFlowService: OAuthFlowService,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly oauthRepository: OAuthClientRepository,
|
||||
private readonly profilesRepository: ProfilesRepository
|
||||
private readonly profilesRepository: ProfilesRepository,
|
||||
private readonly usersService: UsersService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request & { authMethod: AuthMethods }) {
|
||||
async authenticate(request: ApiAuthGuardRequest) {
|
||||
try {
|
||||
const { params } = request;
|
||||
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||
@@ -45,7 +51,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (oAuthClientId && oAuthClientSecret) {
|
||||
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
||||
}
|
||||
|
||||
if (bearerToken) {
|
||||
@@ -53,7 +59,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? AuthMethods["API_KEY"]
|
||||
: AuthMethods["ACCESS_TOKEN"];
|
||||
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
||||
}
|
||||
|
||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||
@@ -61,7 +67,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (nextAuthToken) {
|
||||
request.authMethod = AuthMethods["NEXT_AUTH"];
|
||||
return await this.authenticateNextAuth(nextAuthToken);
|
||||
return await this.authenticateNextAuth(nextAuthToken, request);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(NO_AUTH_PROVIDED_MESSAGE);
|
||||
@@ -75,8 +81,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateNextAuth(token: { email?: string | null }) {
|
||||
const user = await this.nextAuthStrategy(token);
|
||||
async authenticateNextAuth(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
const user = await this.nextAuthStrategy(token, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
@@ -87,12 +93,16 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
};
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
async authenticateOAuthClient(
|
||||
oAuthClientId: string,
|
||||
oAuthClientSecret: string,
|
||||
request: ApiAuthGuardRequest
|
||||
) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string, request: ApiAuthGuardRequest) {
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!client) {
|
||||
@@ -115,14 +125,20 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException("No user associated with the provided OAuth client");
|
||||
}
|
||||
|
||||
request.organizationId = client.organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async authenticateBearerToken(authString: string, requestOrigin: string | undefined) {
|
||||
async authenticateBearerToken(
|
||||
authString: string,
|
||||
request: ApiAuthGuardRequest,
|
||||
requestOrigin: string | undefined
|
||||
) {
|
||||
try {
|
||||
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? await this.apiKeyStrategy(authString)
|
||||
: await this.accessTokenStrategy(authString, requestOrigin);
|
||||
? await this.apiKeyStrategy(authString, request)
|
||||
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
||||
|
||||
if (!user) {
|
||||
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||
@@ -139,7 +155,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async apiKeyStrategy(apiKey: string) {
|
||||
async apiKeyStrategy(apiKey: string, request: ApiAuthGuardRequest) {
|
||||
const isLicenseValid = await this.deploymentsService.checkLicense();
|
||||
if (!isLicenseValid) {
|
||||
throw new UnauthorizedException("Invalid or missing CALCOM_LICENSE_KEY environment variable");
|
||||
@@ -163,10 +179,12 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(apiKeyOwnerId);
|
||||
request.organizationId = keyData.teamId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async accessTokenStrategy(accessToken: string, origin?: string) {
|
||||
async accessTokenStrategy(accessToken: string, request: ApiAuthGuardRequest, origin?: string) {
|
||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||
if (!accessTokenValid) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
@@ -190,10 +208,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication api key not found.");
|
||||
}
|
||||
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async nextAuthStrategy(token: { email?: string | null }) {
|
||||
async nextAuthStrategy(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
if (!token.email) {
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
@@ -202,6 +227,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -26,8 +27,14 @@ import { Module } from "@nestjs/common";
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
||||
exports: [BillingService, BillingRepository],
|
||||
providers: [
|
||||
BillingConfigService,
|
||||
BillingService,
|
||||
BillingRepository,
|
||||
BillingProcessor,
|
||||
ManagedOrganizationsBillingService,
|
||||
],
|
||||
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
|
||||
controllers: [BillingController],
|
||||
})
|
||||
export class BillingModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Process, Processor } from "@nestjs/bull";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
@@ -38,7 +38,7 @@ export class BillingRepository {
|
||||
|
||||
async updateBillingOverdue(subId: string, cusId: string, overdue: boolean) {
|
||||
try {
|
||||
return this.dbWrite.prisma.platformBilling.update({
|
||||
return this.dbWrite.prisma.platformBilling.updateMany({
|
||||
where: {
|
||||
subscriptionId: subId,
|
||||
customerId: cusId,
|
||||
|
||||
@@ -141,7 +141,6 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/bi
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { InjectQueue } from "@nestjs/bull";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ManagedOrganizationsBillingService {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createManagedOrganizationBilling(managerOrgId: number, managedOrgId: number) {
|
||||
const managerOrgBilling = await this.dbRead.prisma.platformBilling.findUnique({
|
||||
where: { id: managerOrgId },
|
||||
});
|
||||
if (!managerOrgBilling) {
|
||||
throw new NotFoundException("Manager organization billing not found.");
|
||||
}
|
||||
if (managerOrgBilling.plan !== PlatformPlan.SCALE) {
|
||||
throw new NotFoundException("Manager organization billing plan is not SCALE.");
|
||||
}
|
||||
|
||||
return this.dbWrite.prisma.platformBilling.create({
|
||||
data: {
|
||||
id: managedOrgId,
|
||||
customerId: managerOrgBilling.customerId,
|
||||
subscriptionId: managerOrgBilling.subscriptionId,
|
||||
plan: managerOrgBilling.plan,
|
||||
billingCycleStart: managerOrgBilling.billingCycleStart,
|
||||
billingCycleEnd: managerOrgBilling.billingCycleEnd,
|
||||
overdue: managerOrgBilling.overdue,
|
||||
managerBillingId: managerOrgBilling.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.module";
|
||||
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
|
||||
+60
-18
@@ -3,8 +3,7 @@ import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -15,14 +14,27 @@ import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
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 { NextAuthMockStrategy } from "test/mocks/next-auth-mock.strategy";
|
||||
import { ApiAuthMockStrategy } from "test/mocks/api-auth-mock.strategy";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import {
|
||||
APPS_READ,
|
||||
APPS_WRITE,
|
||||
BOOKING_READ,
|
||||
BOOKING_WRITE,
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
PROFILE_READ,
|
||||
PROFILE_WRITE,
|
||||
SCHEDULE_READ,
|
||||
SCHEDULE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import type { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("OAuth Clients Endpoints", () => {
|
||||
@@ -72,15 +84,15 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
|
||||
})
|
||||
).compile();
|
||||
const strategy = moduleRef.get(NextAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(NextAuthMockStrategy);
|
||||
const strategy = moduleRef.get(ApiAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(ApiAuthMockStrategy);
|
||||
usersFixtures = new UserRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
teamFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
@@ -123,6 +135,7 @@ describe("OAuth Clients Endpoints", () => {
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let teamFixtures: TeamRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
|
||||
let user: User;
|
||||
let org: Team;
|
||||
@@ -130,19 +143,20 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
|
||||
})
|
||||
).compile();
|
||||
const strategy = moduleRef.get(NextAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(NextAuthMockStrategy);
|
||||
const strategy = moduleRef.get(ApiAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(ApiAuthMockStrategy);
|
||||
usersFixtures = new UserRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
teamFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
|
||||
user = await usersFixtures.create({
|
||||
email: userEmail,
|
||||
@@ -218,17 +232,17 @@ describe("OAuth Clients Endpoints", () => {
|
||||
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||
});
|
||||
|
||||
it(`/POST`, () => {
|
||||
it(`/POST`, async () => {
|
||||
const body: CreateOAuthClientInput = {
|
||||
name: oAuthClientName,
|
||||
redirectUris: ["http://test-oauth-client.com"],
|
||||
permissions: 32,
|
||||
permissions: ["BOOKING_READ", "PROFILE_WRITE"],
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/oauth-clients")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
@@ -239,6 +253,9 @@ describe("OAuth Clients Endpoints", () => {
|
||||
clientId: responseBody.data.clientId,
|
||||
clientSecret: responseBody.data.clientSecret,
|
||||
};
|
||||
const dbOAuthClient = await oAuthClientsRepositoryFixture.get(client.clientId);
|
||||
expect(dbOAuthClient).toBeDefined();
|
||||
expect(dbOAuthClient?.permissions).toEqual(BOOKING_READ + PROFILE_WRITE);
|
||||
});
|
||||
});
|
||||
it(`/GET`, () => {
|
||||
@@ -291,7 +308,18 @@ describe("OAuth Clients Endpoints", () => {
|
||||
let membership: Membership;
|
||||
let client: { clientId: string; clientSecret: string };
|
||||
const oAuthClientName = `oauth-clients-owner-${randomString()}`;
|
||||
const oAuthClientPermissions = 32;
|
||||
const oAuthClientPermissions: CreateOAuthClientInput["permissions"] = [
|
||||
"EVENT_TYPE_READ",
|
||||
"EVENT_TYPE_WRITE",
|
||||
"BOOKING_READ",
|
||||
"BOOKING_WRITE",
|
||||
"SCHEDULE_READ",
|
||||
"SCHEDULE_WRITE",
|
||||
"APPS_READ",
|
||||
"APPS_WRITE",
|
||||
"PROFILE_READ",
|
||||
"PROFILE_WRITE",
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
membership = await membershipFixtures.addUserToOrg(user, org, "OWNER", true);
|
||||
@@ -301,13 +329,13 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const body: CreateOAuthClientInput = {
|
||||
name: oAuthClientName,
|
||||
redirectUris: ["http://test-oauth-client.com"],
|
||||
permissions: 32,
|
||||
permissions: ["*"],
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/oauth-clients")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
@@ -318,6 +346,20 @@ describe("OAuth Clients Endpoints", () => {
|
||||
clientId: responseBody.data.clientId,
|
||||
clientSecret: responseBody.data.clientSecret,
|
||||
};
|
||||
const dbOAuthClient = await oAuthClientsRepositoryFixture.get(client.clientId);
|
||||
expect(dbOAuthClient).toBeDefined();
|
||||
expect(dbOAuthClient?.permissions).toEqual(
|
||||
EVENT_TYPE_READ +
|
||||
EVENT_TYPE_WRITE +
|
||||
BOOKING_READ +
|
||||
BOOKING_WRITE +
|
||||
SCHEDULE_READ +
|
||||
SCHEDULE_WRITE +
|
||||
APPS_READ +
|
||||
APPS_WRITE +
|
||||
PROFILE_READ +
|
||||
PROFILE_WRITE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+18
-32
@@ -1,8 +1,7 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||
@@ -10,11 +9,9 @@ import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controller
|
||||
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Body,
|
||||
@@ -33,15 +30,14 @@ import {
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags as DocsTags,
|
||||
ApiExcludeController as DocsExcludeController,
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { Pagination } from "@calcom/platform-types";
|
||||
|
||||
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
@@ -51,15 +47,16 @@ Second, make sure that the logged in user has organizationId set to pass the Org
|
||||
path: "/v2/oauth-clients",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@ApiTags("OAuth Clients")
|
||||
@UseGuards(ApiAuthGuard, OrganizationRolesGuard)
|
||||
export class OAuthClientsController {
|
||||
private readonly logger = new Logger("OAuthClientController");
|
||||
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
private readonly oAuthClientsService: OAuthClientsService,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly teamsRepository: OrganizationsRepository,
|
||||
private usersService: UsersService
|
||||
private readonly teamsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@@ -71,10 +68,9 @@ export class OAuthClientsController {
|
||||
type: CreateOAuthClientResponseDto,
|
||||
})
|
||||
async createOAuthClient(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@GetOrgId() organizationId: number,
|
||||
@Body() body: CreateOAuthClientInput
|
||||
): Promise<CreateOAuthClientResponseDto> {
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
this.logger.log(
|
||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||
);
|
||||
@@ -84,14 +80,11 @@ export class OAuthClientsController {
|
||||
throw new BadRequestException("Team is not subscribed, cannot create an OAuth Client.");
|
||||
}
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
|
||||
const oAuthClientCredentials = await this.oAuthClientsService.createOAuthClient(organizationId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
clientId: id,
|
||||
clientSecret: secret,
|
||||
},
|
||||
data: oAuthClientCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,10 +92,8 @@ export class OAuthClientsController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
|
||||
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
}
|
||||
|
||||
@@ -112,11 +103,7 @@ export class OAuthClientsController {
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
const client = await this.oauthClientRepository.getOAuthClient(clientId);
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -149,8 +136,7 @@ export class OAuthClientsController {
|
||||
@Body() body: UpdateOAuthClientInput
|
||||
): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`);
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, body);
|
||||
|
||||
const client = await this.oAuthClientsService.updateOAuthClient(clientId, body);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -161,7 +147,7 @@ export class OAuthClientsController {
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
|
||||
const client = await this.oauthClientRepository.deleteOAuthClient(clientId);
|
||||
const client = await this.oAuthClientsService.deleteOAuthClient(clientId);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
|
||||
+3
-47
@@ -1,53 +1,9 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsDate,
|
||||
IsNotEmptyObject,
|
||||
} from "class-validator";
|
||||
import { ValidateNested, IsEnum, IsNotEmptyObject } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class PlatformOAuthClientDto {
|
||||
@ApiProperty({ example: "clsx38nbl0001vkhlwin9fmt0" })
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: "MyClient" })
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: "secretValue" })
|
||||
@IsString()
|
||||
secret!: string;
|
||||
|
||||
@ApiProperty({ example: 3 })
|
||||
@IsNumber()
|
||||
permissions!: number;
|
||||
|
||||
@ApiPropertyOptional({ example: "https://example.com/logo.png" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
logo!: string | null;
|
||||
|
||||
@ApiProperty({ example: ["https://example.com/callback"] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
redirectUris!: string[];
|
||||
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsNumber()
|
||||
organizationId!: number;
|
||||
|
||||
@ApiProperty({ example: "2024-03-23T08:33:21.851Z", type: Date })
|
||||
@IsDate()
|
||||
createdAt!: Date;
|
||||
}
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetOAuthClientResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { PlatformOAuthClientDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetOAuthClientsResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { ApiAuthGuardRequest, ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
@@ -15,25 +16,35 @@ export class OAuthClientGuard implements CanActivate {
|
||||
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { user: ApiAuthGuardUser }>();
|
||||
const request = context.switchToHttp().getRequest<ApiAuthGuardRequest>();
|
||||
const organizationId = this.getOrganizationId(context);
|
||||
const user: ApiAuthGuardUser = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const oAuthClientId = request.params.clientId;
|
||||
|
||||
if (!oAuthClientId) {
|
||||
throw new ForbiddenException("No OAuth client associated with the request.");
|
||||
throw new ForbiddenException("OAuthClientGuard - No OAuth client associated with the request.");
|
||||
}
|
||||
|
||||
if (!user || !organizationId) {
|
||||
throw new ForbiddenException("No organization associated with the user.");
|
||||
throw new ForbiddenException("OAuthClientGuard - No organization associated with the user.");
|
||||
}
|
||||
|
||||
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!oAuthClient) {
|
||||
throw new NotFoundException("OAuth client not found.");
|
||||
throw new NotFoundException("OAuthClientGuard - OAuth client not found.");
|
||||
}
|
||||
|
||||
return Boolean(user.isSystemAdmin || oAuthClient.organizationId === organizationId);
|
||||
}
|
||||
|
||||
getOrganizationId(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const authMethodOrganizationId = request.organizationId;
|
||||
if (authMethodOrganizationId) return authMethodOrganizationId;
|
||||
|
||||
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
return userOrganizationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oaut
|
||||
import { OAuthFlowController } from "@/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthClientsInputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-input.service";
|
||||
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
|
||||
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -18,6 +21,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -40,6 +44,10 @@ import { Global, Module } from "@nestjs/common";
|
||||
OAuthFlowService,
|
||||
OAuthClientUsersService,
|
||||
OrganizationsTeamsService,
|
||||
OAuthClientsService,
|
||||
OAuthClientsInputService,
|
||||
OAuthClientsOutputService,
|
||||
JwtService,
|
||||
],
|
||||
controllers: [OAuthClientUsersController, OAuthClientsController, OAuthFlowController],
|
||||
exports: [OAuthClientRepository],
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import type { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import type { PlatformOAuthClient, Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientRepository {
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private jwtService: JwtService
|
||||
) {}
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createOAuthClient(organizationId: number, data: CreateOAuthClientInput) {
|
||||
async createOAuthClient(
|
||||
organizationId: number,
|
||||
data: Omit<Prisma.PlatformOAuthClientCreateInput, "organization">
|
||||
) {
|
||||
return this.dbWrite.prisma.platformOAuthClient.create({
|
||||
data: {
|
||||
...data,
|
||||
secret: this.jwtService.sign(data),
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
@@ -86,7 +81,7 @@ export class OAuthClientRepository {
|
||||
|
||||
async updateOAuthClient(
|
||||
clientId: string,
|
||||
updateData: Partial<CreateOAuthClientInput>
|
||||
updateData: Prisma.PlatformOAuthClientUpdateInput
|
||||
): Promise<PlatformOAuthClient> {
|
||||
return this.dbWrite.prisma.platformOAuthClient.update({
|
||||
where: { id: clientId },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PERMISSION_MAP } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsInputService {
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
transformCreateOAuthClientInput(input: CreateOAuthClientInput) {
|
||||
const transformed = {
|
||||
...input,
|
||||
permissions: this.transformPermissions(input.permissions),
|
||||
};
|
||||
|
||||
return {
|
||||
...transformed,
|
||||
secret: this.jwtService.sign(transformed),
|
||||
};
|
||||
}
|
||||
|
||||
transformPermissions(permissions: Array<keyof typeof PERMISSION_MAP | "*">): number {
|
||||
const values = permissions.includes("*")
|
||||
? Object.values(PERMISSION_MAP)
|
||||
: permissions.map((p) => PERMISSION_MAP[p as keyof typeof PERMISSION_MAP]);
|
||||
return values.reduce((acc, val) => acc | val, 0);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { PERMISSIONS, PERMISSION_MAP } from "@calcom/platform-constants";
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsOutputService {
|
||||
transformPermissions(permissionsNumber: number): Array<keyof typeof PERMISSION_MAP> {
|
||||
const permissionNumbers = PERMISSIONS.filter(
|
||||
(permission) => (permissionsNumber & permission) === permission
|
||||
);
|
||||
|
||||
return permissionNumbers.map(
|
||||
(permission) =>
|
||||
Object.entries(PERMISSION_MAP).find(
|
||||
([_, value]) => value === permission
|
||||
)?.[0] as keyof typeof PERMISSION_MAP
|
||||
);
|
||||
}
|
||||
|
||||
transformOAuthClient(client: PlatformOAuthClient): PlatformOAuthClientDto {
|
||||
return {
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
secret: client.secret,
|
||||
permissions: this.transformPermissions(client.permissions),
|
||||
logo: client.logo,
|
||||
redirectUris: client.redirectUris,
|
||||
organizationId: client.organizationId,
|
||||
createdAt: client.createdAt,
|
||||
bookingRedirectUri: client.bookingRedirectUri ?? undefined,
|
||||
bookingCancelRedirectUri: client.bookingCancelRedirectUri ?? undefined,
|
||||
bookingRescheduleRedirectUri: client.bookingRescheduleRedirectUri ?? undefined,
|
||||
areEmailsEnabled: client.areEmailsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
transformOAuthClients(clients: PlatformOAuthClient[]): PlatformOAuthClientDto[] {
|
||||
return clients.map((client) => this.transformOAuthClient(client));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
|
||||
|
||||
import { OAuthClientRepository } from "../../oauth-client.repository";
|
||||
import { OAuthClientsInputService } from "./oauth-clients-input.service";
|
||||
import { OAuthClientsOutputService } from "./oauth-clients-output.service";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsService {
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
private readonly oauthClientsInputService: OAuthClientsInputService,
|
||||
private readonly oauthClientsOutputService: OAuthClientsOutputService
|
||||
) {}
|
||||
|
||||
async createOAuthClient(organizationId: number, input: CreateOAuthClientInput) {
|
||||
const transformedInput = this.oauthClientsInputService.transformCreateOAuthClientInput(input);
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(
|
||||
organizationId,
|
||||
transformedInput
|
||||
);
|
||||
|
||||
return {
|
||||
clientId: id,
|
||||
clientSecret: secret,
|
||||
};
|
||||
}
|
||||
|
||||
async getOAuthClients(organizationId: number) {
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
return this.oauthClientsOutputService.transformOAuthClients(clients);
|
||||
}
|
||||
|
||||
async getOAuthClientById(clientId: string) {
|
||||
const client = await this.oauthClientRepository.getOAuthClient(clientId);
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
|
||||
async updateOAuthClient(clientId: string, input: UpdateOAuthClientInput) {
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, input);
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
|
||||
async deleteOAuthClient(clientId: string) {
|
||||
const client = await this.oauthClientRepository.deleteOAuthClient(clientId);
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
+7
-7
@@ -6,16 +6,16 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/create-organization-attributes.output";
|
||||
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/delete-organization-attributes.output";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/create-organization-attributes.output";
|
||||
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/delete-organization-attributes.output";
|
||||
import {
|
||||
GetOrganizationAttributesOutput,
|
||||
GetSingleAttributeOutput,
|
||||
} from "@/modules/organizations/outputs/attributes/get-organization-attributes.output";
|
||||
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/update-organization-attributes.output";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||
} from "@/modules/organizations/attributes/index/outputs/get-organization-attributes.output";
|
||||
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/update-organization-attributes.output";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsOptional, ValidateNested } from "class-validator";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { OrganizationAttributesRepository } from "@/modules/organizations/repositories/attributes/organization-attribute.repository";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { OrganizationAttributesRepository } from "@/modules/organizations/attributes/index/organization-attributes.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
+11
-11
@@ -6,19 +6,19 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import {
|
||||
AssignOptionUserOutput,
|
||||
UnassignOptionUserOutput,
|
||||
} from "@/modules/organizations/outputs/attributes/options/assign-option-user.output";
|
||||
import { CreateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/create-option.output";
|
||||
import { DeleteAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/delete-option.output";
|
||||
import { GetOptionUserOutput } from "@/modules/organizations/outputs/attributes/options/get-option-user.output";
|
||||
import { GetAllAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/get-option.output";
|
||||
import { UpdateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/update-option.output";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
|
||||
} from "@/modules/organizations/attributes/options/outputs/assign-option-user.output";
|
||||
import { CreateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/create-option.output";
|
||||
import { DeleteAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/delete-option.output";
|
||||
import { GetOptionUserOutput } from "@/modules/organizations/attributes/options/outputs/get-option-user.output";
|
||||
import { GetAllAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/get-option.output";
|
||||
import { UpdateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/update-option.output";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
@@ -30,7 +30,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Orgs / Attributes / Options")
|
||||
export class OrganizationsOptionsAttributesController {
|
||||
export class OrganizationsAttributesOptionsController {
|
||||
constructor(private readonly organizationsAttributesOptionsService: OrganizationAttributeOptionService) {}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested } from "class-validator";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested } from "class-validator";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/repositories/attributes/organization-attribute-option.repository";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
const TYPE_SUPPORTS_VALUE = new Set(["TEXT", "NUMBER"]);
|
||||
+4
-4
@@ -10,10 +10,10 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/event-types/services/organizations-event-types.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output";
|
||||
import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output";
|
||||
import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { Injectable, PipeTransform } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||
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";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
+32
@@ -4,6 +4,8 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsRepository {
|
||||
constructor(
|
||||
@@ -21,6 +23,17 @@ export class OrganizationsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByIds(organizationIds: number[]) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: organizationIds,
|
||||
},
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdIncludeBilling(orgId: number) {
|
||||
return this.dbRead.prisma.team.findUnique({
|
||||
where: {
|
||||
@@ -132,6 +145,25 @@ export class OrganizationsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async update(organizationId: number, data: Prisma.TeamUpdateInput) {
|
||||
return this.dbWrite.prisma.team.update({
|
||||
where: {
|
||||
id: organizationId,
|
||||
isOrganization: true,
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(organizationId: number) {
|
||||
return this.dbWrite.prisma.team.delete({
|
||||
where: {
|
||||
id: organizationId,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgBySlug(slug: string) {
|
||||
return this.dbRead.prisma.team.findFirst({
|
||||
where: {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "@/modules/organizations/inputs/update-organization-membership.input";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/update-organization-membership.input";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
+9
-9
@@ -7,15 +7,15 @@ import { IsMembershipInOrg } from "@/modules/auth/guards/memberships/is-membersh
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "@/modules/organizations/inputs/update-organization-membership.input";
|
||||
import { CreateOrgMembershipOutput } from "@/modules/organizations/outputs/organization-membership/create-membership.output";
|
||||
import { DeleteOrgMembership } from "@/modules/organizations/outputs/organization-membership/delete-membership.output";
|
||||
import { GetAllOrgMemberships } from "@/modules/organizations/outputs/organization-membership/get-all-memberships.output";
|
||||
import { GetOrgMembership } from "@/modules/organizations/outputs/organization-membership/get-membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { UpdateOrgMembership } from "@/modules/organizations/outputs/organization-membership/update-membership.output";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/update-organization-membership.input";
|
||||
import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/outputs/create-membership.output";
|
||||
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
|
||||
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
|
||||
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsMembershipRepository {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/memberships/outputs/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
@@ -5,43 +5,44 @@ import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
||||
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/repositories/attributes/organization-attribute-option.repository";
|
||||
import { OrganizationAttributesRepository } from "@/modules/organizations/repositories/attributes/organization-attribute.repository";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/repositories/organizations-teams-memberships.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
|
||||
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
|
||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/attributes/index/controllers/organizations-attributes.controller";
|
||||
import { OrganizationAttributesRepository } from "@/modules/organizations/attributes/index/organization-attributes.repository";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service";
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/event-types/services/organizations-event-types.service";
|
||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/teams/index/organizations-teams.controller";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/users/index/controllers/organizations-users.controller";
|
||||
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
|
||||
import { OrganizationsUsersService } from "@/modules/organizations/users/index/services/organizations-users-service";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/users/ooo/controllers/organizations-users-ooo-controller";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/users/ooo/organizations-users-ooo.repository";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/users/ooo/services/organization-users-ooo.service";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/webhooks/controllers/organizations-webhooks.controller";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository";
|
||||
import { OrganizationsWebhooksService } from "@/modules/organizations/webhooks/services/organizations-webhooks.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -63,6 +64,7 @@ import { Module } from "@nestjs/common";
|
||||
EventTypesModule_2024_06_14,
|
||||
TeamsEventTypesModule,
|
||||
TeamsModule,
|
||||
OrganizationsOrganizationsModule,
|
||||
OrganizationsTeamsRoutingFormsModule,
|
||||
],
|
||||
providers: [
|
||||
@@ -126,7 +128,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsEventTypesController,
|
||||
OrganizationsTeamsMembershipsController,
|
||||
OrganizationsAttributesController,
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsAttributesOptionsController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsObject, IsOptional, IsString, Length } from "class-validator";
|
||||
|
||||
export class CreateOrganizationInput extends RefreshApiKeyInput {
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@ApiProperty({ description: "Name of the organization", example: "CalTeam" })
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiPropertyOptional({ type: Object })
|
||||
readonly metadata?: Record<string, unknown>;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsOptional, IsString, Length } from "class-validator";
|
||||
|
||||
export class UpdateOrganizationInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Length(1)
|
||||
@ApiPropertyOptional({ description: "Name of the organization", example: "CalTeam" })
|
||||
readonly name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class ManagedOrganizationsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createManagedOrganization(managerOrganizationId: number, data: Prisma.TeamCreateInput) {
|
||||
return this.dbWrite.prisma.team.create({
|
||||
data: {
|
||||
...data,
|
||||
managedOrganization: {
|
||||
create: {
|
||||
managerOrganization: {
|
||||
connect: { id: managerOrganizationId },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByManagerManagedIds(managerOrganizationId: number, managedOrganizationId: number) {
|
||||
return this.dbRead.prisma.managedOrganization.findUnique({
|
||||
where: {
|
||||
managerOrganizationId_managedOrganizationId: {
|
||||
managerOrganizationId,
|
||||
managedOrganizationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByManagerOrganizationId(managerOrganizationId: number) {
|
||||
return this.dbRead.prisma.managedOrganization.findMany({
|
||||
where: {
|
||||
managerOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { getEnv } from "@/env";
|
||||
import { hashAPIKey, stripApiKey } from "@/lib/api-key";
|
||||
import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output";
|
||||
import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/CreateOAuthClientResponse.dto";
|
||||
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input";
|
||||
import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input";
|
||||
import {
|
||||
ManagedOrganizationWithApiKeyOutput,
|
||||
ManagedOrganizationOutput,
|
||||
} from "@/modules/organizations/organizations/outputs/managed-organization.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformBilling, User } from "@prisma/client";
|
||||
import { advanceTo, clear } from "jest-date-mock";
|
||||
import { DateTime } from "luxon";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
APPS_READ,
|
||||
APPS_WRITE,
|
||||
BOOKING_READ,
|
||||
BOOKING_WRITE,
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
PROFILE_READ,
|
||||
PROFILE_WRITE,
|
||||
SCHEDULE_READ,
|
||||
SCHEDULE_WRITE,
|
||||
X_CAL_SECRET_KEY,
|
||||
} from "@calcom/platform-constants";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse, CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Organizations Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let managerOrg: Team;
|
||||
let managedOrg: ManagedOrganizationWithApiKeyOutput;
|
||||
const managerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`;
|
||||
let managerOrgAdmin: User;
|
||||
let managerOrgAdminApiKey: string;
|
||||
let managerOrgBilling: PlatformBilling;
|
||||
|
||||
let managedOrgApiKey: string;
|
||||
let managedOrgOAuthClientId: string;
|
||||
let managedOrgOAuthClientSecret: string;
|
||||
const createOAuthClientBody: CreateOAuthClientInput = {
|
||||
name: "OAuth client for managed organization",
|
||||
redirectUris: ["http://localhost:4321"],
|
||||
permissions: ["*"],
|
||||
};
|
||||
|
||||
const newDate = new Date(2035, 0, 9, 15, 0, 0);
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
}).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
managerOrgAdmin = await userRepositoryFixture.create({
|
||||
email: managerOrgAdminEmail,
|
||||
username: managerOrgAdminEmail,
|
||||
});
|
||||
|
||||
managerOrg = await organizationsRepositoryFixture.create({
|
||||
name: `organizations-organizations-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: managerOrgAdminEmail,
|
||||
user: { connect: { id: managerOrgAdmin.id } },
|
||||
organization: { connect: { id: managerOrg.id } },
|
||||
movedFromUser: { connect: { id: managerOrgAdmin.id } },
|
||||
});
|
||||
|
||||
managerOrgBilling = await platformBillingRepositoryFixture.create(managerOrg.id, "SCALE");
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: managerOrgAdmin.id } },
|
||||
team: { connect: { id: managerOrg.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
managerOrgAdmin.id,
|
||||
null,
|
||||
managerOrg.id
|
||||
);
|
||||
managerOrgAdminApiKey = `cal_test_${keyString}`;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
advanceTo(newDate);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
it("should create managed organization", async () => {
|
||||
const suffix = randomString();
|
||||
|
||||
const body: CreateOrganizationInput = {
|
||||
name: `organizations organizations org ${suffix}`,
|
||||
metadata: { key: "value" },
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${managerOrg.id}/organizations`)
|
||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ManagedOrganizationWithApiKeyOutput> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
managedOrg = responseBody.data;
|
||||
expect(managedOrg?.id).toBeDefined();
|
||||
expect(managedOrg?.name).toEqual(body.name);
|
||||
expect(managedOrg?.metadata).toEqual(body.metadata);
|
||||
expect(managedOrg?.apiKey).toBeDefined();
|
||||
|
||||
// note(Lauris): check that managed organization is correctly setup in database
|
||||
const managedOrgInDb =
|
||||
await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id);
|
||||
expect(managedOrgInDb).toBeDefined();
|
||||
expect(managedOrgInDb?.id).toEqual(managedOrg.id);
|
||||
expect(managedOrgInDb?.isPlatform).toEqual(true);
|
||||
expect(managedOrgInDb?.isOrganization).toEqual(true);
|
||||
expect(managedOrgInDb?.managedOrganization?.managedOrganizationId).toEqual(managedOrg.id);
|
||||
expect(managedOrgInDb?.managedOrganization?.managerOrganizationId).toEqual(managerOrg.id);
|
||||
// note(Lauris): check that manager organization is correctly setup in database
|
||||
const managerOrgInDb =
|
||||
await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managerOrg.id);
|
||||
expect(managerOrgInDb).toBeDefined();
|
||||
expect(managerOrgInDb?.id).toEqual(managerOrg.id);
|
||||
expect(managerOrgInDb?.isPlatform).toEqual(true);
|
||||
expect(managerOrgInDb?.isOrganization).toEqual(true);
|
||||
expect(managerOrgInDb?.managedOrganization).toEqual(null);
|
||||
expect(managerOrgInDb?.managedOrganizations?.length).toEqual(1);
|
||||
expect(managerOrgInDb?.managedOrganizations?.[0]?.managedOrganizationId).toEqual(managedOrg.id);
|
||||
expect(managerOrgInDb?.managedOrganizations?.[0]?.managerOrganizationId).toEqual(managerOrg.id);
|
||||
|
||||
// note(Lauris): test that auth user who made request to create managed organization is OWNER of it
|
||||
const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(
|
||||
managerOrgAdmin.id,
|
||||
managedOrg.id
|
||||
);
|
||||
expect(membership?.role ?? "").toEqual("OWNER");
|
||||
expect(membership?.accepted).toEqual(true);
|
||||
// note(Lauris): test that auth user who made request to create managed organization has profile in it
|
||||
const profile = await profilesRepositoryFixture.findByOrgIdUserId(managedOrg.id, managerOrgAdmin.id);
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.id).toBeDefined();
|
||||
expect(profile?.username).toEqual(managerOrgAdmin.username);
|
||||
// note(Lauris): check that platform billing is setup correctly for manager and managed orgs
|
||||
const managerOrgBilling = await platformBillingRepositoryFixture.get(managerOrg.id);
|
||||
expect(managerOrgBilling).toBeDefined();
|
||||
expect(managerOrgBilling?.id).toBeDefined();
|
||||
const customerId = managerOrgBilling?.customerId;
|
||||
expect(customerId).toBeDefined();
|
||||
if (!customerId) {
|
||||
throw new Error(
|
||||
"organizations-organizations.controller.e2e-spec.ts: PlatformBilling customerId is not defined"
|
||||
);
|
||||
}
|
||||
const subscriptionId = managerOrgBilling?.subscriptionId;
|
||||
expect(subscriptionId).toBeDefined();
|
||||
if (!subscriptionId) {
|
||||
throw new Error(
|
||||
"organizations-organizations.controller.e2e-spec.ts: PlatformBilling subscriptionId is not defined"
|
||||
);
|
||||
}
|
||||
const plan = managerOrgBilling?.plan;
|
||||
expect(plan).toEqual("SCALE");
|
||||
|
||||
const managedOrgBilling = await platformBillingRepositoryFixture.get(managedOrg.id);
|
||||
expect(managedOrgBilling).toBeDefined();
|
||||
expect(managedOrgBilling?.customerId).toEqual(customerId);
|
||||
expect(managedOrgBilling?.subscriptionId).toEqual(subscriptionId);
|
||||
expect(managedOrgBilling?.plan).toEqual(plan);
|
||||
expect(managedOrgBilling?.managerBillingId).toEqual(managerOrgBilling?.id);
|
||||
|
||||
expect(managerOrgBilling?.managedBillings?.length).toEqual(1);
|
||||
expect(managerOrgBilling?.managedBillings?.[0]?.id).toEqual(managedOrgBilling?.id);
|
||||
|
||||
const billings = await platformBillingRepositoryFixture.getByCustomerSubscriptionIds(
|
||||
customerId,
|
||||
subscriptionId
|
||||
);
|
||||
expect(billings).toBeDefined();
|
||||
// note(Lauris): manager and manager organizaitons billings because managed organization client and subscription ids are same as manager organization billing row.
|
||||
expect(billings?.length).toEqual(2);
|
||||
|
||||
// note(Lauris): check that in database api key is generated for managed org
|
||||
const managedOrgApiKeys = await apiKeysRepositoryFixture.getTeamApiKeys(managedOrg.id);
|
||||
expect(managedOrgApiKeys?.length).toEqual(1);
|
||||
expect(managedOrgApiKeys?.[0]?.id).toBeDefined();
|
||||
const apiKeyPrefix = getEnv("API_KEY_PREFIX", "cal_");
|
||||
const hashedApiKey = `${hashAPIKey(stripApiKey(managedOrg?.apiKey, apiKeyPrefix))}`;
|
||||
expect(managedOrgApiKeys?.[0]?.hashedKey).toEqual(hashedApiKey);
|
||||
const expectedExpiresAt = DateTime.fromJSDate(newDate).setZone("utc").plus({ days: 30 }).toJSDate();
|
||||
expect(managedOrgApiKeys?.[0]?.expiresAt).toEqual(expectedExpiresAt);
|
||||
expect(managedOrgApiKeys?.[0]?.note).toEqual(
|
||||
`Managed organization API key. ManagerOrgId: ${managerOrg.id}. ManagedOrgId: ${managedOrg.id}`
|
||||
);
|
||||
managedOrgApiKey = managedOrg?.apiKey;
|
||||
});
|
||||
});
|
||||
|
||||
it("should get managed organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`)
|
||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ManagedOrganizationOutput> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseManagedOrg = responseBody.data;
|
||||
expect(responseManagedOrg?.id).toBeDefined();
|
||||
expect(responseManagedOrg?.name).toEqual(managedOrg.name);
|
||||
expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get managed organizations", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${managerOrg.id}/organizations`)
|
||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ManagedOrganizationOutput[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseManagedOrgs = responseBody.data;
|
||||
expect(responseManagedOrgs?.length).toEqual(1);
|
||||
const responseManagedOrg = responseManagedOrgs[0];
|
||||
expect(responseManagedOrg?.id).toBeDefined();
|
||||
expect(responseManagedOrg?.name).toEqual(managedOrg.name);
|
||||
expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update managed organization ", async () => {
|
||||
const suffix = randomString();
|
||||
|
||||
const newOrgName = `new organizations organizations org ${suffix}`;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`)
|
||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||
.send({
|
||||
name: newOrgName,
|
||||
} satisfies UpdateOrganizationInput)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ManagedOrganizationWithApiKeyOutput> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseManagedOrg = responseBody.data;
|
||||
expect(responseManagedOrg?.id).toBeDefined();
|
||||
expect(responseManagedOrg?.name).toEqual(newOrgName);
|
||||
|
||||
const managedOrgInDb =
|
||||
await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id);
|
||||
expect(managedOrgInDb).toBeDefined();
|
||||
expect(managedOrgInDb?.name).toEqual(newOrgName);
|
||||
|
||||
managedOrg = { ...managedOrg, name: newOrgName };
|
||||
});
|
||||
});
|
||||
|
||||
it("should refresh api key for managed organization with a custom duration", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/api-keys/refresh`)
|
||||
.send({ apiKeyDaysValid: 60 })
|
||||
.set("Authorization", `Bearer ${managedOrgApiKey}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: RefreshApiKeyOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseData = responseBody.data;
|
||||
const newApiKey = responseData?.apiKey;
|
||||
expect(newApiKey).toBeDefined();
|
||||
expect(newApiKey).not.toEqual(managedOrgApiKey);
|
||||
|
||||
const managedOrgApiKeys = await apiKeysRepositoryFixture.getTeamApiKeys(managedOrg.id);
|
||||
expect(managedOrgApiKeys?.length).toEqual(1);
|
||||
expect(managedOrgApiKeys?.[0]?.id).toBeDefined();
|
||||
const apiKeyPrefix = getEnv("API_KEY_PREFIX", "cal_");
|
||||
const hashedApiKey = `${hashAPIKey(stripApiKey(newApiKey, apiKeyPrefix))}`;
|
||||
expect(managedOrgApiKeys?.[0]?.hashedKey).toEqual(hashedApiKey);
|
||||
const expectedExpiresAt = DateTime.fromJSDate(newDate).setZone("utc").plus({ days: 60 }).toJSDate();
|
||||
expect(managedOrgApiKeys?.[0]?.expiresAt).toEqual(expectedExpiresAt);
|
||||
expect(managedOrgApiKeys?.[0]?.note).toEqual(
|
||||
`Managed organization API key. ManagerOrgId: ${managerOrg.id}. ManagedOrgId: ${managedOrg.id}`
|
||||
);
|
||||
managedOrgApiKey = newApiKey;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create OAuth client for managed organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/oauth-clients`)
|
||||
.send(createOAuthClientBody)
|
||||
.set("Authorization", `Bearer ${managedOrgApiKey}`)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateOAuthClientResponseDto = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseData = responseBody.data;
|
||||
const clientId = responseData?.clientId;
|
||||
const clientSecret = responseData?.clientSecret;
|
||||
expect(clientId).toBeDefined();
|
||||
expect(clientSecret).toBeDefined();
|
||||
|
||||
const managedOrgOAuthClients = await oAuthClientsRepositoryFixture.getByOrgId(managedOrg.id);
|
||||
expect(managedOrgOAuthClients?.length).toEqual(1);
|
||||
expect(managedOrgOAuthClients?.[0]?.id).toBeDefined();
|
||||
expect(managedOrgOAuthClients?.[0]?.id).toEqual(clientId);
|
||||
expect(managedOrgOAuthClients?.[0]?.secret).toEqual(clientSecret);
|
||||
expect(managedOrgOAuthClients?.[0]?.name).toEqual(createOAuthClientBody.name);
|
||||
expect(managedOrgOAuthClients?.[0]?.redirectUris).toEqual(createOAuthClientBody.redirectUris);
|
||||
expect(managedOrgOAuthClients?.[0]?.permissions).toEqual(
|
||||
EVENT_TYPE_READ +
|
||||
EVENT_TYPE_WRITE +
|
||||
BOOKING_READ +
|
||||
BOOKING_WRITE +
|
||||
SCHEDULE_READ +
|
||||
SCHEDULE_WRITE +
|
||||
APPS_READ +
|
||||
APPS_WRITE +
|
||||
PROFILE_READ +
|
||||
PROFILE_WRITE
|
||||
);
|
||||
managedOrgOAuthClientId = clientId;
|
||||
managedOrgOAuthClientSecret = clientSecret;
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch OAuth client for managed organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/oauth-clients/${managedOrgOAuthClientId}`)
|
||||
.set(X_CAL_SECRET_KEY, managedOrgOAuthClientSecret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetOAuthClientResponseDto = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseData = responseBody.data;
|
||||
expect(responseData?.id).toEqual(managedOrgOAuthClientId);
|
||||
expect(responseData?.secret).toEqual(managedOrgOAuthClientSecret);
|
||||
expect(responseData?.name).toEqual(createOAuthClientBody.name);
|
||||
expect(responseData?.redirectUris).toEqual(createOAuthClientBody.redirectUris);
|
||||
expect(responseData?.permissions).toEqual([
|
||||
"EVENT_TYPE_READ",
|
||||
"EVENT_TYPE_WRITE",
|
||||
"BOOKING_READ",
|
||||
"BOOKING_WRITE",
|
||||
"SCHEDULE_READ",
|
||||
"SCHEDULE_WRITE",
|
||||
"APPS_READ",
|
||||
"APPS_WRITE",
|
||||
"PROFILE_READ",
|
||||
"PROFILE_WRITE",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete managed organization ", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`)
|
||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ManagedOrganizationWithApiKeyOutput> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseManagedOrg = responseBody.data;
|
||||
expect(responseManagedOrg?.id).toBeDefined();
|
||||
expect(responseManagedOrg?.id).toEqual(managedOrg.id);
|
||||
expect(responseManagedOrg?.name).toEqual(managedOrg.name);
|
||||
|
||||
const managedOrgInDb =
|
||||
await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id);
|
||||
expect(managedOrgInDb).toEqual(null);
|
||||
|
||||
const billings = await platformBillingRepositoryFixture.getByCustomerSubscriptionIds(
|
||||
managerOrgBilling.customerId,
|
||||
managerOrgBilling.subscriptionId!
|
||||
);
|
||||
expect(billings).toBeDefined();
|
||||
// note(Lauris): only manager billing is left
|
||||
expect(billings?.length).toEqual(1);
|
||||
|
||||
const managerOrgInDb =
|
||||
await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managerOrg.id);
|
||||
expect(managerOrgInDb).toBeDefined();
|
||||
expect(managerOrgInDb?.id).toEqual(managerOrg.id);
|
||||
expect(managerOrgInDb?.managedOrganizations?.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(managerOrgAdmin.email);
|
||||
await organizationsRepositoryFixture.delete(managerOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user