feat: platform team event-types (#15928)
* fix: dont check OrganizationSettings.orgAutoAcceptEmail uniqueness for platform teams * feat: display organizationId in OAuthClientCard * feat: ApiAuthStrategy handle oauth credentials * refactor: clean up ApiAuthStrategy test * refactor: use ApiAuthGuard in OauthClientsUsersController and OAuthFlowController * fix: copying org id * refactor: more specific error message when api auth * refactor: auto accept team creator membership * feat: useTeamEventType and useTeamEventTypes hooks * refactor: make team event-types public & searchable by eventSlug * feat: include host name in team event-types hosts response * fix: isFixed=true by default for COLLECTIVE event hosts * fix: useTeamEventType eventSlug access * feat: BookerPlatformWrapper enable team events by exposting orgId and teamId props * refactor: provide orgId in atoms context * refactor: use orgId from context in team event-types hooks * refactor: return teams in useMe * chore: examples app teams setup * Revert "refactor: return teams in useMe" This reverts commit de992ddc9af6ee9a2111938069f5b9c34cc2d8ea. * Revert "chore: examples app teams setup" This reverts commit 0766aa21acc25efa2361d38c3f87ddba773a0245. * feat: useTeams hook * chore: setup examples app with team event-type * fix: small fixes * swagger * Revert "refactor: provide orgId in atoms context" This reverts commit f053a498ee6f8fa8ece5ec8d8630c59eda8873e3. * feat: orgId in atoms context * feat: PlatformBilling guard * chore: delete test of the deleted oauth-client-credentials.guard * refactor: org event-types collective events isFixed always true and priority medium * refactor: org event-types COLLECTIVE response ignore isFixed and priority as they are same * fix: organizations event-types e2e * fix: billing guard * refactor: tests cleanup * fix: platform plan guard spec * refactor: e2e test cleanup * seed error if not team * refactor: rename authenticateApiKey to authenticateBearerToken * refactor: transforming response hosts * refactor: rename findUniqueByMatchingAutoAcceptEmail to findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail * refactor: rename findUniqueByMatchingAutoAcceptEmail to findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
Morgan
parent
06bb7055ac
commit
2f946fc5da
@@ -22,4 +22,7 @@ export class MeOutput {
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
|
||||
@IsInt()
|
||||
organizationId!: number | null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.
|
||||
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -21,6 +22,7 @@ import { PassportModule } from "@nestjs/passport";
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
DeploymentsModule,
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||
exports: [NextAuthGuard, ApiAuthGuard],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
export const PlatformPlan = Reflector.createDecorator<PlatformPlanType>();
|
||||
@@ -0,0 +1,123 @@
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
describe("PlatformPlanGuard", () => {
|
||||
let guard: PlatformPlanGuard;
|
||||
let reflector: Reflector;
|
||||
let organizationsRepository: OrganizationsRepository;
|
||||
let redisService: RedisService;
|
||||
|
||||
const mockContext = createMockExecutionContext({
|
||||
params: { teamId: "1", orgId: "1" },
|
||||
user: { id: "1" },
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
reflector = new Reflector();
|
||||
organizationsRepository = createMock<OrganizationsRepository>();
|
||||
redisService = createMock<RedisService>({
|
||||
redis: {
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
set: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
guard = new PlatformPlanGuard(reflector, organizationsRepository, redisService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(guard).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return true", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: true,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
platformBilling: {
|
||||
subscriptionId: "sub_123",
|
||||
plan: "SCALE",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the organization does not exist", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if the organization is not platform", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: false,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
platformBilling: undefined,
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the organization has no subscription", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: true,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
platformBilling: {
|
||||
subscriptionId: null,
|
||||
plan: "STARTER",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if the user has a lower plan than required", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: true,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
platformBilling: {
|
||||
subscriptionId: "sub_123",
|
||||
plan: "STARTER",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if the result is cached in Redis", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(redisService.redis, "get").mockResolvedValue(JSON.stringify(true));
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the result is cached in Redis", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(redisService.redis, "get").mockResolvedValue(JSON.stringify(false));
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(context: Record<string, string | object>): ExecutionContext {
|
||||
return createMock<ExecutionContext>({
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
params: context.params,
|
||||
user: context.user,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class PlatformPlanGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly organizationsRepository: OrganizationsRepository,
|
||||
private readonly redisService: RedisService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const teamId = request.params.teamId as string;
|
||||
const orgId = request.params.orgId as string;
|
||||
const user = request.user as GetUserReturnType;
|
||||
const minimumPlan = this.reflector.get(PlatformPlan, context.getHandler()) as PlatformPlanType;
|
||||
|
||||
const REDIS_CACHE_KEY = `apiv2:user:${user?.id ?? "none"}:org:${orgId ?? "none"}:team:${
|
||||
teamId ?? "none"
|
||||
}:guard:platformbilling:${minimumPlan}`;
|
||||
|
||||
const cachedAccess = JSON.parse((await this.redisService.redis.get(REDIS_CACHE_KEY)) ?? "false");
|
||||
|
||||
if (cachedAccess) {
|
||||
return cachedAccess;
|
||||
}
|
||||
|
||||
let canAccess = false;
|
||||
|
||||
if (user && orgId) {
|
||||
const team = await this.organizationsRepository.findByIdIncludeBilling(Number(orgId));
|
||||
const isPlatform = team?.isPlatform;
|
||||
const hasSubscription = team?.platformBilling?.subscriptionId;
|
||||
|
||||
if (!team) {
|
||||
canAccess = false;
|
||||
} else if (!isPlatform) {
|
||||
canAccess = true;
|
||||
} else if (!hasSubscription) {
|
||||
canAccess = false;
|
||||
} else {
|
||||
canAccess = hasMinimumPlan({
|
||||
currentPlan: team.platformBilling?.plan as PlatformPlanType,
|
||||
minimumPlan: minimumPlan,
|
||||
plans: ["STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.redisService.redis.set(REDIS_CACHE_KEY, String(canAccess), "EX", 300);
|
||||
return canAccess;
|
||||
}
|
||||
}
|
||||
|
||||
type HasMinimumPlanProp = {
|
||||
currentPlan: PlatformPlanType;
|
||||
minimumPlan: PlatformPlanType;
|
||||
plans: PlatformPlanType[];
|
||||
};
|
||||
|
||||
export function hasMinimumPlan(props: HasMinimumPlanProp): boolean {
|
||||
const currentPlanIndex = props.plans.indexOf(props.currentPlan);
|
||||
const minimumPlanIndex = props.plans.indexOf(props.minimumPlan);
|
||||
|
||||
if (currentPlanIndex === -1 || minimumPlanIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid platform billing plan provided. Current plan: ${props.currentPlan}, Minimum plan: ${props.minimumPlan}`
|
||||
);
|
||||
}
|
||||
|
||||
return currentPlanIndex >= minimumPlanIndex;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||
@@ -17,11 +18,14 @@ import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { MockedRedisService } from "test/mocks/mock-redis-service";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -33,11 +37,17 @@ describe("ApiAuthStrategy", () => {
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
const validApiKeyEmail = "api-key-user-email@example.com";
|
||||
const validAccessTokenEmail = "access-token-user-email@example.com";
|
||||
const validOAuthEmail = "oauth-user@example.com";
|
||||
|
||||
let validApiKeyUser: User;
|
||||
let validAccessTokenUser: User;
|
||||
let validOAuthUser: User;
|
||||
let module: TestingModule;
|
||||
|
||||
beforeAll(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
@@ -46,6 +56,7 @@ describe("ApiAuthStrategy", () => {
|
||||
isGlobal: true,
|
||||
load: [appConfig],
|
||||
}),
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [
|
||||
MockedRedisService,
|
||||
@@ -71,6 +82,7 @@ describe("ApiAuthStrategy", () => {
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(module);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
validApiKeyUser = await userRepositoryFixture.create({
|
||||
email: validApiKeyEmail,
|
||||
@@ -78,6 +90,18 @@ describe("ApiAuthStrategy", () => {
|
||||
validAccessTokenUser = await userRepositoryFixture.create({
|
||||
email: validAccessTokenEmail,
|
||||
});
|
||||
|
||||
validOAuthUser = await userRepositoryFixture.create({
|
||||
email: validOAuthEmail,
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: validOAuthEmail,
|
||||
user: { connect: { id: validOAuthUser.id } },
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
@@ -94,22 +118,9 @@ describe("ApiAuthStrategy", () => {
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer ${accessToken}`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const user = await strategy.accessTokenStrategy(accessToken);
|
||||
await expect(user).toBeDefined();
|
||||
if (user) await expect(user.id).toEqual(validAccessTokenUser.id);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validAccessTokenUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key", async () => {
|
||||
@@ -117,22 +128,15 @@ describe("ApiAuthStrategy", () => {
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: `Bearer cal_test_${keyString}`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer cal_test_${keyString}`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const user = await strategy.apiKeyStrategy(keyString);
|
||||
await expect(user).toBeDefined();
|
||||
if (user) expect(user.id).toEqual(validApiKeyUser.id);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client", async () => {
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
});
|
||||
|
||||
it("should throw 401 if api key is invalid", async () => {
|
||||
@@ -158,7 +162,55 @@ describe("ApiAuthStrategy", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if Authorization header does not contain auth token", async () => {
|
||||
it("should throw 401 if OAuth ID is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
[X_CAL_CLIENT_ID]: `${oAuthClient.id}gibberish`,
|
||||
[X_CAL_SECRET_KEY]: `secret`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if OAuth secret is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
[X_CAL_CLIENT_ID]: `${oAuthClient.id}`,
|
||||
[X_CAL_SECRET_KEY]: `gibberish`,
|
||||
},
|
||||
get: (key: string) =>
|
||||
({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if request does not contain Bearer token nor OAuth client credentials", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
@@ -179,8 +231,11 @@ describe("ApiAuthStrategy", () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(validApiKeyEmail);
|
||||
await userRepositoryFixture.deleteByEmail(validAccessTokenEmail);
|
||||
await oAuthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await userRepositoryFixture.delete(validApiKeyUser.id);
|
||||
await userRepositoryFixture.delete(validAccessTokenUser.id);
|
||||
await userRepositoryFixture.delete(validOAuthUser.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
module.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,9 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { 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 { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
@@ -10,7 +12,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
@@ -20,19 +22,74 @@ 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: ApiKeyRepository,
|
||||
private readonly oauthRepository: OAuthClientRepository,
|
||||
private readonly profilesRepository: ProfilesRepository
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request) {
|
||||
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!authString) {
|
||||
return this.error(new UnauthorizedException("No Authorization header provided"));
|
||||
try {
|
||||
const { params } = request;
|
||||
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||
const oAuthClientId = params.clientId || request.get(X_CAL_CLIENT_ID);
|
||||
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
|
||||
|
||||
if (oAuthClientId && oAuthClientSecret) {
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
||||
}
|
||||
|
||||
if (bearerToken) {
|
||||
const requestOrigin = request.get("Origin");
|
||||
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
"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"
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return this.error(err);
|
||||
}
|
||||
return this.error(
|
||||
new InternalServerErrorException("An error occurred while authenticating the request")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
return this.success(user);
|
||||
}
|
||||
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!client) {
|
||||
throw new UnauthorizedException(`Client with ID ${oAuthClientId} not found`);
|
||||
}
|
||||
|
||||
const requestOrigin = request.get("Origin");
|
||||
if (client.secret !== oAuthClientSecret) {
|
||||
throw new UnauthorizedException("Invalid client secret");
|
||||
}
|
||||
|
||||
const platformCreatorId = await this.profilesRepository.getPlatformOwnerUserId(client.organizationId);
|
||||
|
||||
if (!platformCreatorId) {
|
||||
throw new UnauthorizedException("No owner ID found for this OAuth client");
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findByIdWithProfile(platformCreatorId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("No user associated with the provided OAuth client");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async authenticateBearerToken(authString: string, requestOrigin: string | undefined) {
|
||||
try {
|
||||
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? await this.apiKeyStrategy(authString)
|
||||
|
||||
@@ -4,3 +4,5 @@ export enum PlatformPlan {
|
||||
SCALE = "SCALE",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
|
||||
export type PlatformPlanType = "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE";
|
||||
|
||||
+21
@@ -18,6 +18,7 @@ import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -78,9 +79,13 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let postResponseData: CreateUserResponse;
|
||||
|
||||
const platformAdminEmail = "platform-sensei@mail.com";
|
||||
let platformAdmin: User;
|
||||
|
||||
const userEmail = "oauth-client-user@gmail.com";
|
||||
const userTimeZone = "Europe/Rome";
|
||||
|
||||
@@ -98,9 +103,20 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: userEmail,
|
||||
user: { connect: { id: platformAdmin.id } },
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -277,6 +293,11 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||
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";
|
||||
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
||||
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -37,7 +37,7 @@ import { Pagination } from "@calcom/platform-types";
|
||||
path: "/v2/oauth-clients/:clientId/users",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@DocsTags("Managed users")
|
||||
export class OAuthClientUsersController {
|
||||
private readonly logger = new Logger("UserController");
|
||||
|
||||
+10
@@ -15,6 +15,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { withNextAuth } from "test/utils/withNextAuth";
|
||||
@@ -58,6 +59,7 @@ describe("OAuthFlow Endpoints", () => {
|
||||
let usersRepositoryFixtures: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let user: User;
|
||||
let organization: Team;
|
||||
@@ -83,11 +85,19 @@ describe("OAuthFlow Endpoints", () => {
|
||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
user = await usersRepositoryFixtures.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization" });
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: userEmail,
|
||||
user: { connect: { id: user.id } },
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
||||
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
|
||||
import { OAuthAuthorizeInput } from "@/modules/oauth-clients/inputs/authorize.input";
|
||||
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth-clients/inputs/exchange-code.input";
|
||||
import { RefreshTokenInput } from "@/modules/oauth-clients/inputs/refresh-token.input";
|
||||
@@ -137,7 +137,7 @@ export class OAuthFlowController {
|
||||
|
||||
@Post("/refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async refreshAccessToken(
|
||||
@Param("clientId") clientId: string,
|
||||
@Headers(X_CAL_SECRET_KEY) secretKey: string,
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
import { AppModule } from "@/app.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team } from "@prisma/client";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
|
||||
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { OAuthClientCredentialsGuard } from "./oauth-client-credentials.guard";
|
||||
|
||||
describe("OAuthClientCredentialsGuard", () => {
|
||||
let guard: OAuthClientCredentialsGuard;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let oauthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule, OAuthClientModule],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<OAuthClientCredentialsGuard>(OAuthClientCredentialsGuard);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
oauthClient = await oauthClientRepositoryFixture.create(organization.id, data, secret);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(guard).toBeDefined();
|
||||
expect(oauthClient).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return true if client ID and secret are valid", async () => {
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: oauthClient.secret },
|
||||
{ clientId: oauthClient.id }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if client ID is invalid", async () => {
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: oauthClient.secret },
|
||||
{ clientId: "invalid id" }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it("should return false if secret key is invalid", async () => {
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: "invalid secret" },
|
||||
{ clientId: oauthClient.id }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oauthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, string>
|
||||
): ExecutionContext {
|
||||
return createMock<ExecutionContext>({
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers,
|
||||
params,
|
||||
get: (headerName: string) => headers[headerName],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientCredentialsGuard implements CanActivate {
|
||||
constructor(private readonly oauthRepository: OAuthClientRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const { params } = request;
|
||||
|
||||
const oauthClientId = params.clientId;
|
||||
const oauthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||
|
||||
if (!oauthClientId) {
|
||||
throw new UnauthorizedException("Missing client ID");
|
||||
}
|
||||
if (!oauthClientSecret) {
|
||||
throw new UnauthorizedException("Missing client secret");
|
||||
}
|
||||
|
||||
const client = await this.oauthRepository.getOAuthClient(oauthClientId);
|
||||
|
||||
if (!client || client.secret !== oauthClientSecret) {
|
||||
throw new UnauthorizedException("Invalid client credentials");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
|
||||
import { OAuthFlowController } from "@/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller";
|
||||
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -34,14 +33,8 @@ import { Global, Module } from "@nestjs/common";
|
||||
BillingModule,
|
||||
SchedulesModule_2024_04_15,
|
||||
],
|
||||
providers: [
|
||||
OAuthClientRepository,
|
||||
OAuthClientCredentialsGuard,
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
OAuthClientUsersService,
|
||||
],
|
||||
providers: [OAuthClientRepository, TokensRepository, OAuthFlowService, OAuthClientUsersService],
|
||||
controllers: [OAuthClientUsersController, OAuthClientsController, OAuthFlowController],
|
||||
exports: [OAuthClientRepository, OAuthClientCredentialsGuard],
|
||||
exports: [OAuthClientRepository],
|
||||
})
|
||||
export class OAuthClientModule {}
|
||||
|
||||
+28
-8
@@ -1,7 +1,9 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
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";
|
||||
@@ -32,6 +34,7 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
GetTeamEventTypesQuery_2024_06_14,
|
||||
SkipTakePagination,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
@@ -40,13 +43,13 @@ import {
|
||||
path: "/v2/organizations/:orgId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@DocsTags("Organizations Event Types")
|
||||
export class OrganizationsEventTypesController {
|
||||
constructor(private readonly organizationsEventTypesService: OrganizationsEventTypesService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@Post("/teams/:teamId/event-types")
|
||||
async createTeamEventType(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@@ -68,7 +71,8 @@ export class OrganizationsEventTypesController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@Get("/teams/:teamId/event-types/:eventTypeId")
|
||||
async getTeamEventType(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@@ -86,10 +90,22 @@ export class OrganizationsEventTypesController {
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@UseGuards(IsOrgGuard, IsTeamInOrg)
|
||||
@Get("/teams/:teamId/event-types")
|
||||
async getTeamEventTypes(@Param("teamId", ParseIntPipe) teamId: number): Promise<GetTeamEventTypesOutput> {
|
||||
async getTeamEventTypes(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
|
||||
): Promise<GetTeamEventTypesOutput> {
|
||||
const { eventSlug } = queryParams;
|
||||
if (eventSlug) {
|
||||
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType ? [eventType] : [],
|
||||
};
|
||||
}
|
||||
|
||||
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId);
|
||||
|
||||
return {
|
||||
@@ -99,6 +115,8 @@ export class OrganizationsEventTypesController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@Get("/teams/event-types")
|
||||
async getTeamsEventTypes(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -114,7 +132,8 @@ export class OrganizationsEventTypesController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@Patch("/teams/:teamId/event-types/:eventTypeId")
|
||||
async updateTeamEventType(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@@ -136,7 +155,8 @@ export class OrganizationsEventTypesController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@Delete("/teams/:teamId/event-types/:eventTypeId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async deleteTeamEventType(
|
||||
|
||||
+2
-6
@@ -252,13 +252,9 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
hosts: [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "high",
|
||||
},
|
||||
{
|
||||
userId: teammate2.id,
|
||||
mandatory: false,
|
||||
priority: "low",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -431,8 +427,6 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "medium",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -603,7 +597,9 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
await userRepositoryFixture.deleteByEmail(teammate2.email);
|
||||
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
|
||||
await teamsRepositoryFixture.delete(team.id);
|
||||
await teamsRepositoryFixture.delete(falseTestTeam.id);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await organizationsRepositoryFixture.delete(falseTestOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+8
-1
@@ -1,7 +1,9 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetMembership } from "@/modules/auth/decorators/get-membership/get-membership.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsMembershipInOrg } from "@/modules/auth/guards/memberships/is-membership-in-org.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
@@ -39,12 +41,13 @@ import { Membership } from "@calcom/prisma/client";
|
||||
path: "/v2/organizations/:orgId/memberships",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@DocsTags("Organizations Memberships")
|
||||
export class OrganizationsMembershipsController {
|
||||
constructor(private organizationsMembershipService: OrganizationsMembershipService) {}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getAllMemberships(
|
||||
@@ -66,6 +69,7 @@ export class OrganizationsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async createMembership(
|
||||
@@ -80,6 +84,7 @@ export class OrganizationsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Get("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -91,6 +96,7 @@ export class OrganizationsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Delete("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -107,6 +113,7 @@ export class OrganizationsMembershipsController {
|
||||
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Patch("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateMembership(
|
||||
|
||||
+9
-1
@@ -1,7 +1,9 @@
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
@@ -38,7 +40,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
path: "/v2/organizations/:orgId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@DocsTags("Organizations Schedules")
|
||||
export class OrganizationsSchedulesController {
|
||||
constructor(
|
||||
@@ -47,6 +49,7 @@ export class OrganizationsSchedulesController {
|
||||
) {}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/schedules")
|
||||
async getOrganizationSchedules(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -63,6 +66,7 @@ export class OrganizationsSchedulesController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@Post("/users/:userId/schedules")
|
||||
async createUserSchedule(
|
||||
@@ -78,6 +82,7 @@ export class OrganizationsSchedulesController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@Get("/users/:userId/schedules/:scheduleId")
|
||||
async getUserSchedule(
|
||||
@@ -93,6 +98,7 @@ export class OrganizationsSchedulesController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@Get("/users/:userId/schedules")
|
||||
async getUserSchedules(
|
||||
@@ -107,6 +113,7 @@ export class OrganizationsSchedulesController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@Patch("/users/:userId/schedules/:scheduleId")
|
||||
async updateUserSchedule(
|
||||
@@ -123,6 +130,7 @@ export class OrganizationsSchedulesController {
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@Delete("/users/:userId/schedules/:scheduleId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
+8
-1
@@ -1,6 +1,8 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
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";
|
||||
@@ -38,7 +40,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
path: "/v2/organizations/:orgId/teams/:teamId/memberships",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
|
||||
@DocsTags("Organizations Teams")
|
||||
export class OrganizationsTeamsMembershipsController {
|
||||
constructor(
|
||||
@@ -50,6 +52,7 @@ export class OrganizationsTeamsMembershipsController {
|
||||
@ApiOperation({ summary: "Get all the memberships of a team of an organization." })
|
||||
@UseGuards()
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getAllOrgTeamMemberships(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -75,6 +78,7 @@ export class OrganizationsTeamsMembershipsController {
|
||||
@ApiOperation({ summary: "Get the membership of an organization's team by ID" })
|
||||
@UseGuards()
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getOrgTeamMembership(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -93,6 +97,7 @@ export class OrganizationsTeamsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Delete("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Delete the membership of an organization's team by ID" })
|
||||
@@ -113,6 +118,7 @@ export class OrganizationsTeamsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Patch("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Update the membership of an organization's team by ID" })
|
||||
@@ -135,6 +141,7 @@ export class OrganizationsTeamsMembershipsController {
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: "Create a membership of an organization's team" })
|
||||
|
||||
+10
-2
@@ -1,8 +1,10 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
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";
|
||||
@@ -10,7 +12,6 @@ import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organiza
|
||||
import {
|
||||
OrgMeTeamOutputDto,
|
||||
OrgMeTeamsOutputResponseDto,
|
||||
OrgTeamOutputDto,
|
||||
OrgTeamOutputResponseDto,
|
||||
OrgTeamsOutputResponseDto,
|
||||
} from "@/modules/organizations/outputs/organization-team.output";
|
||||
@@ -32,6 +33,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { OrgTeamOutputDto } from "@calcom/platform-types";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -39,7 +41,7 @@ import { Team } from "@calcom/prisma/client";
|
||||
path: "/v2/organizations/:orgId/teams",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@DocsTags("Organizations Teams")
|
||||
export class OrganizationsTeamsController {
|
||||
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
|
||||
@@ -47,6 +49,7 @@ export class OrganizationsTeamsController {
|
||||
@Get()
|
||||
@ApiOperation({ summary: "Get all the teams of an organization." })
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
async getAllTeams(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
@@ -62,6 +65,7 @@ export class OrganizationsTeamsController {
|
||||
@Get("/me")
|
||||
@ApiOperation({ summary: "Get the organization's teams user is a member of" })
|
||||
@Roles("ORG_MEMBER")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
async getMyTeams(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() queryParams: SkipTakePagination,
|
||||
@@ -88,6 +92,7 @@ export class OrganizationsTeamsController {
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/:teamId")
|
||||
@ApiOperation({ summary: "Get a team of the organization by ID." })
|
||||
async getTeam(@GetTeam() team: Team): Promise<OrgTeamOutputResponseDto> {
|
||||
@@ -99,6 +104,7 @@ export class OrganizationsTeamsController {
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Delete("/:teamId")
|
||||
@ApiOperation({ summary: "Delete a team of the organization by ID." })
|
||||
async deleteTeam(
|
||||
@@ -114,6 +120,7 @@ export class OrganizationsTeamsController {
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Patch("/:teamId")
|
||||
@ApiOperation({ summary: "Update a team of the organization by ID." })
|
||||
async updateTeam(
|
||||
@@ -130,6 +137,7 @@ export class OrganizationsTeamsController {
|
||||
|
||||
@Post()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@ApiOperation({ summary: "Create a team for an organization." })
|
||||
async createTeam(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
|
||||
+7
-1
@@ -1,8 +1,10 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
@@ -39,7 +41,7 @@ import { Team } from "@calcom/prisma/client";
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Organizations Users")
|
||||
export class OrganizationsUsersController {
|
||||
@@ -47,6 +49,7 @@ export class OrganizationsUsersController {
|
||||
|
||||
@Get()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
async getOrganizationsUsers(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() query: GetOrganizationsUsersInput
|
||||
@@ -66,6 +69,7 @@ export class OrganizationsUsersController {
|
||||
|
||||
@Post()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
async createOrganizationUser(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@GetOrg() org: Team,
|
||||
@@ -85,6 +89,7 @@ export class OrganizationsUsersController {
|
||||
|
||||
@Patch("/:userId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
async updateOrganizationUser(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@@ -101,6 +106,7 @@ export class OrganizationsUsersController {
|
||||
|
||||
@Delete("/:userId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
async deleteOrganizationUser(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
|
||||
@@ -1,122 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Length,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class OrgTeamOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
readonly parentId?: number;
|
||||
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@Expose()
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly isOrganization?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly hideBookATeamMember?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly metadata?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly weekStart?: string = "Sunday";
|
||||
}
|
||||
import { OrgTeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
export class OrgMeTeamOutputDto extends OrgTeamOutputDto {
|
||||
@IsString()
|
||||
|
||||
+12
@@ -16,6 +16,18 @@ export class OrganizationsEventTypesRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: {
|
||||
teamId_slug: {
|
||||
teamId,
|
||||
slug: eventTypeSlug,
|
||||
},
|
||||
},
|
||||
include: { users: true, schedule: true, hosts: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamEventTypes(teamId: number) {
|
||||
return this.dbRead.prisma.eventType.findMany({
|
||||
where: {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
HostPriority,
|
||||
SchedulingType,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -30,7 +31,9 @@ export class InputOrganizationsEventTypesService {
|
||||
|
||||
const teamEventType = {
|
||||
...eventType,
|
||||
hosts: assignAllTeamMembers ? await this.getAllTeamMembers(teamId) : this.transformInputHosts(hosts),
|
||||
hosts: assignAllTeamMembers
|
||||
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
|
||||
: this.transformInputHosts(hosts, inputEventType.schedulingType),
|
||||
assignAllTeamMembers,
|
||||
metadata,
|
||||
};
|
||||
@@ -46,6 +49,11 @@ export class InputOrganizationsEventTypesService {
|
||||
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
|
||||
|
||||
const eventType = this.inputEventTypesService.transformInputUpdateEventType(rest);
|
||||
const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId);
|
||||
|
||||
if (!dbEventType) {
|
||||
throw new BadRequestException("Event type to update not found");
|
||||
}
|
||||
|
||||
const children = await this.getChildEventTypesForManagedEventType(eventTypeId, inputEventType, teamId);
|
||||
const teamEventType = {
|
||||
@@ -53,8 +61,8 @@ export class InputOrganizationsEventTypesService {
|
||||
// note(Lauris): we don't populate hosts for managed event-types because they are handled by the children
|
||||
hosts: !children
|
||||
? assignAllTeamMembers
|
||||
? await this.getAllTeamMembers(teamId)
|
||||
: this.transformInputHosts(hosts)
|
||||
? await this.getAllTeamMembers(teamId, dbEventType.schedulingType)
|
||||
: this.transformInputHosts(hosts, dbEventType.schedulingType)
|
||||
: undefined,
|
||||
assignAllTeamMembers,
|
||||
children,
|
||||
@@ -118,28 +126,34 @@ export class InputOrganizationsEventTypesService {
|
||||
});
|
||||
}
|
||||
|
||||
async getAllTeamMembers(teamId: number) {
|
||||
async getAllTeamMembers(teamId: number, schedulingType: SchedulingType | null) {
|
||||
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
|
||||
const isFixed = schedulingType === "COLLECTIVE" ? true : false;
|
||||
|
||||
return membersIds.map((id) => ({
|
||||
userId: id,
|
||||
isFixed: false,
|
||||
isFixed,
|
||||
priority: 2,
|
||||
}));
|
||||
}
|
||||
|
||||
transformInputHosts(inputHosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
|
||||
transformInputHosts(
|
||||
inputHosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined,
|
||||
schedulingType: SchedulingType | null
|
||||
) {
|
||||
if (!inputHosts) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultMandatory = false;
|
||||
const defaultPriority = "medium";
|
||||
const defaultIsFixed = false;
|
||||
|
||||
return inputHosts.map((host) => ({
|
||||
userId: host.userId,
|
||||
isFixed: host.mandatory || defaultMandatory,
|
||||
priority: getPriorityValue(host.priority || defaultPriority),
|
||||
isFixed: schedulingType === "COLLECTIVE" ? true : host.mandatory || defaultIsFixed,
|
||||
priority: getPriorityValue(
|
||||
schedulingType === "COLLECTIVE" ? "medium" : host.priority || defaultPriority
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -98,6 +98,19 @@ export class OrganizationsEventTypesService {
|
||||
return this.outputService.getResponseTeamEventType(eventType);
|
||||
}
|
||||
|
||||
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
|
||||
const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug(
|
||||
teamId,
|
||||
eventTypeSlug
|
||||
);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.outputService.getResponseTeamEventType(eventType);
|
||||
}
|
||||
|
||||
async getTeamEventTypes(teamId: number) {
|
||||
const eventTypes = await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, Host } from "@prisma/client";
|
||||
|
||||
import { HostPriority } from "@calcom/platform-types";
|
||||
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
@@ -47,7 +49,8 @@ type Input = Pick<
|
||||
export class OutputOrganizationsEventTypesService {
|
||||
constructor(
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository
|
||||
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async getResponseTeamEventType(databaseEventType: Input) {
|
||||
@@ -60,7 +63,7 @@ export class OutputOrganizationsEventTypesService {
|
||||
const hosts =
|
||||
databaseEventType.schedulingType === "MANAGED"
|
||||
? await this.getManagedEventTypeHosts(databaseEventType.id)
|
||||
: this.transformHosts(databaseEventType.hosts);
|
||||
: await this.transformHosts(databaseEventType.hosts, databaseEventType.schedulingType);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
@@ -74,25 +77,41 @@ export class OutputOrganizationsEventTypesService {
|
||||
|
||||
async getManagedEventTypeHosts(eventTypeId: number) {
|
||||
const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventTypeId);
|
||||
const hostsIds: number[] = [];
|
||||
const transformedHosts: TeamEventTypeResponseHost[] = [];
|
||||
for (const child of children) {
|
||||
if (child.userId) {
|
||||
hostsIds.push(child.userId);
|
||||
const user = await this.usersRepository.findById(child.userId);
|
||||
transformedHosts.push({ userId: child.userId, name: user?.name || "" });
|
||||
}
|
||||
}
|
||||
return hostsIds.map((userId) => ({ userId }));
|
||||
return transformedHosts;
|
||||
}
|
||||
|
||||
transformHosts(hosts: Host[]) {
|
||||
if (!hosts) return [];
|
||||
async transformHosts(
|
||||
databaseHosts: Host[],
|
||||
schedulingType: SchedulingType | null
|
||||
): Promise<TeamEventTypeResponseHost[]> {
|
||||
if (!schedulingType) return [];
|
||||
|
||||
return hosts.map((host) => {
|
||||
return {
|
||||
userId: host.userId,
|
||||
mandatory: host.isFixed,
|
||||
priority: getPriorityLabel(host.priority || 2),
|
||||
};
|
||||
});
|
||||
const transformedHosts: TeamEventTypeResponseHost[] = [];
|
||||
const databaseUsers = await this.usersRepository.findByIds(databaseHosts.map((host) => host.userId));
|
||||
|
||||
for (const databaseHost of databaseHosts) {
|
||||
const databaseUser = databaseUsers.find((u) => u.id === databaseHost.userId);
|
||||
if (schedulingType === "ROUND_ROBIN") {
|
||||
// note(Lauris): round robin is the only team event where mandatory (isFixed) and priority are used
|
||||
transformedHosts.push({
|
||||
userId: databaseHost.userId,
|
||||
name: databaseUser?.name || "",
|
||||
mandatory: databaseHost.isFixed,
|
||||
priority: getPriorityLabel(databaseHost.priority || 2),
|
||||
});
|
||||
} else {
|
||||
transformedHosts.push({ userId: databaseHost.userId, name: databaseUser?.name || "" });
|
||||
}
|
||||
}
|
||||
|
||||
return transformedHosts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ProfilesRepository],
|
||||
exports: [ProfilesRepository],
|
||||
})
|
||||
export class ProfilesModule {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ProfilesRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
|
||||
async getPlatformOwnerUserId(organizationId: number) {
|
||||
const profile = await this.dbRead.prisma.profile.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return profile?.userId;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,16 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByIds(userIds: number[]) {
|
||||
return this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdWithCalendars(userId: number) {
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -991,6 +991,57 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/teams/me": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsTeamsController_getMyTeams",
|
||||
"summary": "Get the organization's teams user is a member of",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OrgMeTeamsOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Organizations Teams"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/teams/{teamId}": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsTeamsController_getTeam",
|
||||
@@ -1160,6 +1211,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
@@ -1262,6 +1323,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -2301,6 +2372,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -5100,6 +5181,29 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OrgMeTeamsOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OrgTeamOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OrgTeamOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5329,6 +5433,57 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"timeZone",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
"CreateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5377,6 +5532,52 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "One-on-one coaching"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "Europe/Rome"
|
||||
},
|
||||
"availability": {
|
||||
"example": [
|
||||
{
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday"
|
||||
],
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"overrides": {
|
||||
"example": [
|
||||
{
|
||||
"date": "2024-05-20",
|
||||
"startTime": "12:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6012,9 +6213,12 @@
|
||||
"freq"
|
||||
]
|
||||
},
|
||||
"Host": {
|
||||
"TeamEventTypeResponseHost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "number"
|
||||
},
|
||||
@@ -6028,6 +6232,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
@@ -6145,7 +6350,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/TeamEventTypeResponseHost"
|
||||
}
|
||||
},
|
||||
"assignAllTeamMembers": {
|
||||
@@ -6467,6 +6672,26 @@
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
"GetDefaultScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateAvailabilityInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6764,6 +6989,66 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"schedule": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dateOverrides": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeZone",
|
||||
"name",
|
||||
"isDefault",
|
||||
"schedule"
|
||||
]
|
||||
},
|
||||
"EventTypeModel_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7033,6 +7318,10 @@
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
},
|
||||
"organizationId": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7042,7 +7331,8 @@
|
||||
"timeFormat",
|
||||
"defaultScheduleId",
|
||||
"weekStart",
|
||||
"timeZone"
|
||||
"timeZone",
|
||||
"organizationId"
|
||||
]
|
||||
},
|
||||
"GetMeOutput": {
|
||||
|
||||
@@ -46,6 +46,7 @@ export const OAuthClientsList = ({ oauthClients, isDeleting, handleDelete }: OAu
|
||||
isLoading={isDeleting}
|
||||
onDelete={handleDelete}
|
||||
areEmailsEnabled={client.areEmailsEnabled}
|
||||
organizationId={client.organizationId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -22,6 +22,7 @@ type OAuthClientCardProps = {
|
||||
secret: string;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
organizationId: number;
|
||||
};
|
||||
|
||||
export const OAuthClientCard = ({
|
||||
@@ -38,6 +39,7 @@ export const OAuthClientCard = ({
|
||||
onDelete,
|
||||
isLoading,
|
||||
areEmailsEnabled,
|
||||
organizationId,
|
||||
}: OAuthClientCardProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -113,6 +115,21 @@ export const OAuthClientCard = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="font-semibold">Organization Id:</div>
|
||||
<div>{organizationId}</div>
|
||||
<Icon
|
||||
name="clipboard"
|
||||
type="button"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(organizationId.toString());
|
||||
showToast("Organization id copied to clipboard.", "success");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle flex text-sm">
|
||||
<span className="font-semibold">Permissions: </span>
|
||||
{permissions ? <div className="flex">{clientPermissions}</div> : <> Disabled</>}
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("moveUserToMatchingOrg", () => {
|
||||
});
|
||||
|
||||
it("should not proceed if no matching organization is found", async () => {
|
||||
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeNoMatch();
|
||||
organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeNoMatch();
|
||||
|
||||
await moveUserToMatchingOrg({ email });
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("moveUserToMatchingOrg", () => {
|
||||
requestedSlug: "requested-test-org",
|
||||
};
|
||||
|
||||
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeReturnOrganization(
|
||||
organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization(
|
||||
org,
|
||||
{ email }
|
||||
);
|
||||
@@ -65,7 +65,7 @@ describe("moveUserToMatchingOrg", () => {
|
||||
requestedSlug: "requested-test-org",
|
||||
};
|
||||
|
||||
organizationScenarios.OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.fakeReturnOrganization(
|
||||
organizationScenarios.OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.fakeReturnOrganization(
|
||||
org,
|
||||
{ email }
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email";
|
||||
|
||||
// TODO: To be unit tested
|
||||
export async function moveUserToMatchingOrg({ email }: { email: string }) {
|
||||
const org = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({ email });
|
||||
const org = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email });
|
||||
|
||||
if (!org) {
|
||||
return;
|
||||
|
||||
@@ -14,20 +14,22 @@ const OrganizationRepository = organizationMock.OrganizationRepository;
|
||||
|
||||
export const organizationScenarios = {
|
||||
OrganizationRepository: {
|
||||
findUniqueByMatchingAutoAcceptEmail: {
|
||||
findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fakeReturnOrganization: (org: any, forInput: any) => {
|
||||
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.mockImplementation((arg) => {
|
||||
if (forInput.email === arg.email) {
|
||||
return org;
|
||||
OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockImplementation(
|
||||
(arg) => {
|
||||
if (forInput.email === arg.email) {
|
||||
return org;
|
||||
}
|
||||
const errorMsg = "Mock Error-fakeReturnOrganization: Unhandled input";
|
||||
console.log(errorMsg, { arg, forInput });
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
const errorMsg = "Mock Error-fakeReturnOrganization: Unhandled input";
|
||||
console.log(errorMsg, { arg, forInput });
|
||||
throw new Error(errorMsg);
|
||||
});
|
||||
);
|
||||
},
|
||||
fakeNoMatch: () => {
|
||||
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail.mockResolvedValue(null);
|
||||
OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockResolvedValue(null);
|
||||
},
|
||||
},
|
||||
} satisfies Partial<Record<keyof OrganizationModule["OrganizationRepository"], unknown>>,
|
||||
|
||||
@@ -8,7 +8,7 @@ vi.mock("./teamUtils", () => ({
|
||||
getParsedTeam: (org: any) => org,
|
||||
}));
|
||||
|
||||
describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
describe("Organization.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetAllMocks();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -17,7 +17,7 @@ describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
});
|
||||
|
||||
it("should return null if no organization matches the email domain", async () => {
|
||||
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
|
||||
const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({
|
||||
email: "test@example.com",
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
await createReviewedOrganization({ name: "Test Org 2", orgAutoAcceptEmail: "example.com" });
|
||||
|
||||
await expect(
|
||||
OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({ email: "test@example.com" })
|
||||
OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email: "test@example.com" })
|
||||
).rejects.toThrow("Multiple organizations found with the same auto accept email domain");
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
orgAutoAcceptEmail: "example.com",
|
||||
});
|
||||
|
||||
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
|
||||
const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({
|
||||
email: "test@example.com",
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
it("should not confuse a team with organization", async () => {
|
||||
await createTeam({ name: "Test Team", orgAutoAcceptEmail: "example.com" });
|
||||
|
||||
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
|
||||
const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({
|
||||
email: "test@example.com",
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("Organization.findUniqueByMatchingAutoAcceptEmail", () => {
|
||||
it("should correctly match orgAutoAcceptEmail", async () => {
|
||||
await createReviewedOrganization({ name: "Test Org", orgAutoAcceptEmail: "noexample.com" });
|
||||
|
||||
const result = await OrganizationRepository.findUniqueByMatchingAutoAcceptEmail({
|
||||
const result = await OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({
|
||||
email: "test@example.com",
|
||||
});
|
||||
|
||||
|
||||
@@ -165,11 +165,12 @@ export class OrganizationRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findUniqueByMatchingAutoAcceptEmail({ email }: { email: string }) {
|
||||
static async findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }: { email: string }) {
|
||||
const emailDomain = email.split("@").at(-1);
|
||||
const orgs = await prisma.team.findMany({
|
||||
where: {
|
||||
isOrganization: true,
|
||||
isPlatform: false,
|
||||
organizationSettings: {
|
||||
orgAutoAcceptEmail: emailDomain,
|
||||
isOrganizationVerified: true,
|
||||
|
||||
@@ -22,8 +22,12 @@ import type {
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { transformApiEventTypeForAtom } from "../event-types/atom-api-transformers/transformApiEventTypeForAtom";
|
||||
import {
|
||||
transformApiEventTypeForAtom,
|
||||
transformApiTeamEventTypeForAtom,
|
||||
} from "../event-types/atom-api-transformers/transformApiEventTypeForAtom";
|
||||
import { useEventType } from "../hooks/event-types/public/useEventType";
|
||||
import { useTeamEventType } from "../hooks/event-types/public/useTeamEventType";
|
||||
import { useAtomsContext } from "../hooks/useAtomsContext";
|
||||
import { useAvailableSlots } from "../hooks/useAvailableSlots";
|
||||
import { useCalendarsBusyTimes } from "../hooks/useCalendarsBusyTimes";
|
||||
@@ -44,7 +48,7 @@ import { AtomsWrapper } from "../src/components/atoms-wrapper";
|
||||
export type BookerPlatformWrapperAtomProps = Omit<BookerProps, "username" | "entity"> & {
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
username: string | string[];
|
||||
username: string | string[] | undefined;
|
||||
entity?: BookerProps["entity"];
|
||||
// values for the booking form and booking fields
|
||||
defaultFormValues?: {
|
||||
@@ -68,6 +72,7 @@ export type BookerPlatformWrapperAtomProps = Omit<BookerProps, "username" | "ent
|
||||
onDeleteSlotSuccess?: (data: ApiSuccessResponseWithoutData) => void;
|
||||
onDeleteSlotError?: (data: ApiErrorResponse) => void;
|
||||
locationUrl?: string;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => {
|
||||
@@ -88,7 +93,10 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const username = useMemo(() => {
|
||||
return formatUsername(props.username);
|
||||
if (props.username) {
|
||||
return formatUsername(props.username);
|
||||
}
|
||||
return "";
|
||||
}, [props.username]);
|
||||
|
||||
setSelectedDuration(props.duration ?? null);
|
||||
@@ -98,16 +106,45 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
|
||||
return getUsernameList(username ?? "").length > 1;
|
||||
}, [username]);
|
||||
|
||||
const { isSuccess, isError, isPending, data } = useEventType(username, props.eventSlug);
|
||||
const { isSuccess, isError, isPending, data } = useEventType(username, props.eventSlug, props.isTeamEvent);
|
||||
const {
|
||||
isSuccess: isTeamSuccess,
|
||||
isError: isTeamError,
|
||||
isPending: isTeamPending,
|
||||
data: teamData,
|
||||
} = useTeamEventType(props.teamId, props.eventSlug, props.isTeamEvent);
|
||||
|
||||
const event = useMemo(() => {
|
||||
if (props.isTeamEvent) {
|
||||
return {
|
||||
isSuccess: isTeamSuccess,
|
||||
isError: isTeamError,
|
||||
isPending: isTeamPending,
|
||||
data:
|
||||
teamData && teamData.length > 0
|
||||
? transformApiTeamEventTypeForAtom(teamData[0], props.entity)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isSuccess,
|
||||
isError,
|
||||
isPending,
|
||||
data: data && data.length > 0 ? transformApiEventTypeForAtom(data[0], props.entity) : undefined,
|
||||
};
|
||||
}, [isSuccess, isError, isPending, data, props.entity]);
|
||||
}, [
|
||||
props.isTeamEvent,
|
||||
props.entity,
|
||||
isSuccess,
|
||||
isError,
|
||||
isPending,
|
||||
data,
|
||||
isTeamSuccess,
|
||||
isTeamError,
|
||||
isTeamPending,
|
||||
teamData,
|
||||
]);
|
||||
|
||||
if (isDynamic && props.duration && event.data) {
|
||||
// note(Lauris): Mandatory - In case of "dynamic" event type default event duration returned by the API is 30,
|
||||
@@ -188,7 +225,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
|
||||
// Should only wait for one or the other, not both.
|
||||
(Boolean(eventSlug) || Boolean(event?.data?.id) || event?.data?.id === 0),
|
||||
orgSlug: props.entity?.orgSlug ?? undefined,
|
||||
eventTypeSlug: isDynamic ? "dynamic" : undefined,
|
||||
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
|
||||
});
|
||||
|
||||
const bookerForm = useBookingForm({
|
||||
|
||||
@@ -9,6 +9,7 @@ import frTranslations from "@calcom/web/public/static/locales/fr/common.json";
|
||||
import ptBrTranslations from "@calcom/web/public/static/locales/pt-BR/common.json";
|
||||
|
||||
import { AtomsContext } from "../hooks/useAtomsContext";
|
||||
import { useMe } from "../hooks/useMe";
|
||||
import { useOAuthClient } from "../hooks/useOAuthClient";
|
||||
import { useOAuthFlow } from "../hooks/useOAuthFlow";
|
||||
import { useTimezone } from "../hooks/useTimezone";
|
||||
@@ -38,6 +39,7 @@ export function BaseCalProvider({
|
||||
onTimezoneChange,
|
||||
}: CalProviderProps) {
|
||||
const [error, setError] = useState<string>("");
|
||||
const { data: me } = useMe();
|
||||
|
||||
const { mutateAsync } = useUpdateUserTimezone();
|
||||
|
||||
@@ -117,6 +119,7 @@ export function BaseCalProvider({
|
||||
isInit: isInit,
|
||||
isValidClient: Boolean(!error && clientId && isInit),
|
||||
isAuth: Boolean(isInit && !error && clientId && currentAccessToken && http.getAuthorizationHeader()),
|
||||
organizationId: me?.data.organizationId || 0,
|
||||
...translations,
|
||||
}}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
@@ -134,6 +137,7 @@ export function BaseCalProvider({
|
||||
isInit: false,
|
||||
isRefreshing: false,
|
||||
...translations,
|
||||
organizationId: 0,
|
||||
}}>
|
||||
<>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
|
||||
+86
-1
@@ -7,7 +7,7 @@ import {
|
||||
transformApiEventTypeBookingFields,
|
||||
} from "@calcom/lib/event-types/transformers";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
import {
|
||||
bookerLayoutOptions,
|
||||
BookerLayouts,
|
||||
@@ -91,6 +91,91 @@ export function transformApiEventTypeForAtom(
|
||||
};
|
||||
}
|
||||
|
||||
export function transformApiTeamEventTypeForAtom(
|
||||
eventType: TeamEventTypeOutput_2024_06_14,
|
||||
entity: BookerProps["entity"] | undefined
|
||||
) {
|
||||
const { lengthInMinutes, locations, hosts, bookingFields, ...rest } = eventType;
|
||||
|
||||
const isDefault = isDefaultEvent(rest.title);
|
||||
|
||||
const defaultEventBookerLayouts = {
|
||||
enabledLayouts: [...bookerLayoutOptions],
|
||||
defaultLayout: BookerLayouts.MONTH_VIEW,
|
||||
};
|
||||
const firstUsersMetadata = userMetadataSchema.parse({});
|
||||
const bookerLayouts = bookerLayoutsSchema.parse(
|
||||
firstUsersMetadata?.defaultBookerLayouts || defaultEventBookerLayouts
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: getLocations(locations),
|
||||
bookingFields: getBookingFields(bookingFields),
|
||||
isDefault,
|
||||
isDynamic: false,
|
||||
profile: {
|
||||
username: "team",
|
||||
name: "team",
|
||||
weekStart: "Sunday",
|
||||
image: "",
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
theme: null,
|
||||
bookerLayouts,
|
||||
},
|
||||
entity: entity
|
||||
? {
|
||||
...entity,
|
||||
orgSlug: entity.orgSlug || null,
|
||||
teamSlug: entity.teamSlug || null,
|
||||
fromRedirectOfNonOrgLink: true,
|
||||
name: entity.name || null,
|
||||
logoUrl: entity.logoUrl || undefined,
|
||||
}
|
||||
: {
|
||||
fromRedirectOfNonOrgLink: true,
|
||||
considerUnpublished: false,
|
||||
orgSlug: null,
|
||||
teamSlug: null,
|
||||
name: null,
|
||||
logoUrl: undefined,
|
||||
},
|
||||
hosts: hosts.map((host) => ({
|
||||
user: {
|
||||
id: host.userId,
|
||||
avatarUrl: null,
|
||||
name: host.name,
|
||||
username: "",
|
||||
metadata: {},
|
||||
darkBrandColor: null,
|
||||
brandColor: null,
|
||||
theme: null,
|
||||
weekStart: "Sunday",
|
||||
},
|
||||
})),
|
||||
users: hosts.map((host) => ({
|
||||
metadata: undefined,
|
||||
bookerUrl: getBookerBaseUrlSync(null),
|
||||
profile: {
|
||||
username: "",
|
||||
name: host.name,
|
||||
weekStart: "Sunday",
|
||||
image: "",
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
theme: null,
|
||||
organization: null,
|
||||
id: host.userId,
|
||||
organizationId: null,
|
||||
userId: host.userId,
|
||||
upId: `usr-${host.userId}`,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function isDefaultEvent(eventSlug: string) {
|
||||
const foundInDefaults = defaultEvents.find((obj) => {
|
||||
return obj.slug === eventSlug;
|
||||
|
||||
@@ -13,7 +13,7 @@ import http from "../../../lib/http";
|
||||
export const QUERY_KEY = "use-event-type";
|
||||
export type UsePublicEventReturnType = ReturnType<typeof useEventType>;
|
||||
|
||||
export const useEventType = (username: string, eventSlug: string) => {
|
||||
export const useEventType = (username: string, eventSlug: string, isTeamEvent: boolean | undefined) => {
|
||||
const [stateUsername, stateEventSlug] = useBookerStore(
|
||||
(state) => [state.username, state.eventSlug],
|
||||
shallow
|
||||
@@ -29,6 +29,10 @@ export const useEventType = (username: string, eventSlug: string) => {
|
||||
const event = useQuery({
|
||||
queryKey: [QUERY_KEY, stateUsername ?? username, stateEventSlug ?? eventSlug],
|
||||
queryFn: async () => {
|
||||
if (isTeamEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
return http
|
||||
.get<ApiResponse<EventTypeOutput_2024_06_14[]>>(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiSuccessResponse, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
import { useAtomsContext } from "../../useAtomsContext";
|
||||
|
||||
export const QUERY_KEY = "use-team-event-type";
|
||||
|
||||
export const useTeamEventType = (teamId: number | undefined, eventSlug: string, isTeamEvent: boolean | undefined) => {
|
||||
const { organizationId } = useAtomsContext();
|
||||
|
||||
const [stateEventSlug] = useBookerStore(
|
||||
(state) => [state.eventSlug],
|
||||
shallow
|
||||
);
|
||||
|
||||
const requestEventSlug = stateEventSlug ?? eventSlug;
|
||||
|
||||
const pathname = `/organizations/${organizationId}/teams/${teamId}/event-types?eventSlug=${requestEventSlug}`;
|
||||
|
||||
const event = useQuery({
|
||||
queryKey: [QUERY_KEY, eventSlug, organizationId, teamId],
|
||||
queryFn: async () => {
|
||||
if(organizationId && teamId && eventSlug && isTeamEvent) {
|
||||
return http?.get<ApiResponse<TeamEventTypeOutput_2024_06_14[]>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return event;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
import { useAtomsContext } from "../../useAtomsContext";
|
||||
|
||||
export const QUERY_KEY = "use-team-event-types";
|
||||
export const useTeamEventTypes = (teamId: number) => {
|
||||
const { organizationId } = useAtomsContext();
|
||||
const pathname = `/organizations/${organizationId}/teams/${teamId}/event-types`;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, organizationId, teamId],
|
||||
queryFn: () => {
|
||||
return http?.get<ApiResponse<TeamEventTypeOutput_2024_06_14[]>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!organizationId && !!teamId,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
import type { OrgTeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../lib/http";
|
||||
import { useAtomsContext } from "../useAtomsContext";
|
||||
|
||||
export const QUERY_KEY = "use-teams";
|
||||
|
||||
export const useTeams = () => {
|
||||
const { organizationId } = useAtomsContext();
|
||||
|
||||
const pathname = `/organizations/${organizationId}/teams/me`;
|
||||
|
||||
const event = useQuery({
|
||||
queryKey: [QUERY_KEY, organizationId],
|
||||
queryFn: async () => {
|
||||
return http?.get<ApiResponse<OrgTeamOutputDto[]>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<OrgTeamOutputDto[]>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!organizationId,
|
||||
});
|
||||
|
||||
return event;
|
||||
};
|
||||
@@ -26,11 +26,13 @@ export interface IAtomsContext {
|
||||
locales: CalProviderLanguagesType[];
|
||||
exists: (key: translationKeys | string) => boolean;
|
||||
};
|
||||
organizationId: number;
|
||||
}
|
||||
|
||||
export const AtomsContext = createContext({
|
||||
clientId: "",
|
||||
accessToken: "",
|
||||
organizationId: 0,
|
||||
options: { refreshUrl: "", apiUrl: "" },
|
||||
error: "",
|
||||
getClient: () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export { useIsPlatform } from "./hooks/useIsPlatform";
|
||||
export { useAtomsContext } from "./hooks/useAtomsContext";
|
||||
export { useConnectedCalendars } from "./hooks/useConnectedCalendars";
|
||||
export { useEventTypes } from "./hooks/event-types/public/useEventTypes";
|
||||
export { useTeamEventTypes } from "./hooks/event-types/public/useTeamEventTypes";
|
||||
export { useEventType as useEvent } from "./hooks/event-types/public/useEventType";
|
||||
export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById";
|
||||
export { useCancelBooking } from "./hooks/useCancelBooking";
|
||||
@@ -18,3 +19,4 @@ export { BookerEmbed } from "./booker-embed";
|
||||
export { useDeleteCalendarCredentials } from "./hooks/calendars/useDeleteCalendarCredentials";
|
||||
export { useAddSelectedCalendar } from "./hooks/calendars/useAddSelectedCalendar";
|
||||
export { useRemoveSelectedCalendar } from "./hooks/calendars/useRemoveSelectedCalendar";
|
||||
export { useTeams } from "./hooks/teams/useTeams";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
NEXT_PUBLIC_X_CAL_ID=""
|
||||
X_CAL_SECRET_KEY=""
|
||||
NEXT_PUBLIC_CALCOM_API_URL="http://localhost:5555/api/v2"
|
||||
ORGANIZATION_ID=
|
||||
@@ -26,6 +26,9 @@ function generateRandomEmail() {
|
||||
return `${randomLocalPart}@${randomDomain}`;
|
||||
}
|
||||
|
||||
// note(Lauris): needed because useEffect kicks in twice creating 2 parallel requests
|
||||
let seeding = false;
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [email, setUserEmail] = useState("");
|
||||
@@ -47,16 +50,21 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const randomEmail = generateRandomEmail();
|
||||
fetch("/api/managed-user", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: randomEmail }),
|
||||
}).then(async (res) => {
|
||||
const data = await res.json();
|
||||
setAccessToken(data.accessToken);
|
||||
setUserEmail(data.email);
|
||||
setUsername(data.username);
|
||||
});
|
||||
const randomEmailOne = generateRandomEmail();
|
||||
const randomEmailTwo = generateRandomEmail();
|
||||
if (!seeding) {
|
||||
seeding = true;
|
||||
fetch("/api/managed-user", {
|
||||
method: "POST",
|
||||
|
||||
body: JSON.stringify({ emails: [randomEmailOne, randomEmailTwo] }),
|
||||
}).then(async (res) => {
|
||||
const data = await res.json();
|
||||
setAccessToken(data.accessToken);
|
||||
setUserEmail(data.email);
|
||||
setUsername(data.username);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!!selectedUser) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import prisma from "../../lib/prismaClient";
|
||||
|
||||
@@ -14,7 +14,9 @@ type Data = {
|
||||
|
||||
// example endpoint to create a managed cal.com user
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||
const { email } = JSON.parse(req.body);
|
||||
const { emails } = JSON.parse(req.body);
|
||||
const emailOne = emails[0];
|
||||
const emailTwo = emails[1];
|
||||
|
||||
const existingUser = await prisma.user.findFirst({ orderBy: { createdAt: "desc" } });
|
||||
if (existingUser && existingUser.calcomUserId) {
|
||||
@@ -25,11 +27,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
accessToken: existingUser.accessToken ?? "",
|
||||
});
|
||||
}
|
||||
const localUser = await prisma.user.create({
|
||||
|
||||
const localUserOne = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
email: emailOne,
|
||||
},
|
||||
});
|
||||
|
||||
const localUserTwo = await prisma.user.create({
|
||||
data: {
|
||||
email: emailTwo,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
@@ -42,11 +52,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
email: emailOne,
|
||||
name: "John Jones",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const body = await response.json();
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
@@ -55,9 +66,55 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
calcomUserId: body.data?.user.id,
|
||||
calcomUsername: (body.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUser.id },
|
||||
where: { id: localUserOne.id },
|
||||
});
|
||||
|
||||
const responseTwo = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailTwo,
|
||||
name: "Jane Doe",
|
||||
}),
|
||||
}
|
||||
);
|
||||
const bodyTwo = await responseTwo.json();
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (bodyTwo.data?.refreshToken as string) ?? "",
|
||||
accessToken: (bodyTwo.data?.accessToken as string) ?? "",
|
||||
calcomUserId: bodyTwo.data?.user.id,
|
||||
calcomUsername: (bodyTwo.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserTwo.id },
|
||||
});
|
||||
|
||||
await createDefaultSchedule(body.data?.accessToken as string);
|
||||
await createDefaultSchedule(bodyTwo.data?.accessToken as string);
|
||||
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
const organizationId = process.env.ORGANIZATION_ID;
|
||||
if (!organizationId) {
|
||||
throw new Error("Organization ID is not set");
|
||||
}
|
||||
|
||||
const team = await createTeam(+organizationId, "Team Doe");
|
||||
if (!team) {
|
||||
throw new Error("Failed to create team. Probably your platform team does not have required plan.");
|
||||
}
|
||||
|
||||
await createMembership(+organizationId, team.id, body.data?.user.id);
|
||||
await createMembership(+organizationId, team.id, bodyTwo.data?.user.id);
|
||||
await createCollectiveEventType(+organizationId, team.id, [body.data?.user.id, bodyTwo.data?.user.id]);
|
||||
|
||||
return res.status(200).json({
|
||||
id: body?.data?.user?.id,
|
||||
email: (body.data?.user.email as string) ?? "",
|
||||
@@ -66,6 +123,77 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
});
|
||||
}
|
||||
|
||||
async function createTeam(orgId: number, name: string) {
|
||||
const response = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const body = await response.json();
|
||||
return body.data;
|
||||
}
|
||||
|
||||
async function createMembership(orgId: number, teamId: number, userId: number) {
|
||||
await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams/${teamId}/memberships`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
accepted: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createCollectiveEventType(orgId: number, teamId: number, userIds: number[]) {
|
||||
await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams/${teamId}/event-types`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lengthInMinutes: 60,
|
||||
title: "Doe collective",
|
||||
slug: "doe-collective",
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: userIds.map((userId) => ({ userId })),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createDefaultSchedule(accessToken: string) {
|
||||
const name = "Default Schedule";
|
||||
const timeZone = "Europe/London";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Inter } from "next/font/google";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Booker, useEventTypes } from "@calcom/atoms";
|
||||
import { Booker, useEventTypes, useTeamEventTypes, useTeams } from "@calcom/atoms";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -12,8 +12,11 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
const [bookingTitle, setBookingTitle] = useState<string | null>(null);
|
||||
const [eventTypeSlug, setEventTypeSlug] = useState<string | null>(null);
|
||||
const [eventTypeDuration, setEventTypeDuration] = useState<number | null>(null);
|
||||
const [isTeamEvent, setIsTeamEvent] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypes(props.calUsername);
|
||||
const { data: teams } = useTeams();
|
||||
const { isLoading: isLoadingTeamEvents, data: teamEventTypes } = useTeamEventTypes(teams?.[0]?.id || 0);
|
||||
const rescheduleUid = (router.query.rescheduleUid as string) ?? "";
|
||||
const eventTypeSlugQueryParam = (router.query.eventTypeSlug as string) ?? "";
|
||||
|
||||
@@ -28,6 +31,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
|
||||
{!isLoadingEvents && !eventTypeSlug && Boolean(eventTypes?.length) && !rescheduleUid && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1>User event types</h1>
|
||||
{eventTypes?.map(
|
||||
(event: { id: number; slug: string; title: string; lengthInMinutes: number }) => {
|
||||
const formatEventSlug = event.slug
|
||||
@@ -40,6 +44,36 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
onClick={() => {
|
||||
setEventTypeSlug(event.slug);
|
||||
setEventTypeDuration(event.lengthInMinutes);
|
||||
setIsTeamEvent(false);
|
||||
}}
|
||||
className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4"
|
||||
key={event.id}>
|
||||
<h1 className="text-lg font-semibold">{formatEventSlug}</h1>
|
||||
<p>{`/${event.slug}`}</p>
|
||||
<span className="border-none bg-gray-800 px-2 text-white">{event?.lengthInMinutes}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingTeamEvents && !eventTypeSlug && Boolean(teamEventTypes?.length) && !rescheduleUid && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1>Team event types</h1>
|
||||
{teamEventTypes?.map(
|
||||
(event: { id: number; slug: string; title: string; lengthInMinutes: number }) => {
|
||||
const formatEventSlug = event.slug
|
||||
.split("-")
|
||||
.map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setEventTypeSlug(event.slug);
|
||||
setEventTypeDuration(event.lengthInMinutes);
|
||||
setIsTeamEvent(true);
|
||||
}}
|
||||
className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4"
|
||||
key={event.id}>
|
||||
@@ -54,28 +88,33 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
)}
|
||||
|
||||
{!bookingTitle && eventTypeSlug && !rescheduleUid && (
|
||||
<Booker
|
||||
eventSlug={eventTypeSlug}
|
||||
username={props.calUsername ?? ""}
|
||||
onCreateBookingSuccess={(data) => {
|
||||
setBookingTitle(data.data.title ?? "");
|
||||
router.push(`/${data.data.uid}`);
|
||||
}}
|
||||
duration={eventTypeDuration}
|
||||
customClassNames={{
|
||||
bookerContainer: "!bg-[#F5F2FE] [&_button:!rounded-full] border-subtle border",
|
||||
datePickerCustomClassNames: {
|
||||
datePickerDatesActive: "!bg-[#D7CEF5]",
|
||||
},
|
||||
eventMetaCustomClassNames: {
|
||||
eventMetaTitle: "text-[#7151DC]",
|
||||
},
|
||||
availableTimeSlotsCustomClassNames: {
|
||||
availableTimeSlotsHeaderContainer: "!bg-[#F5F2FE]",
|
||||
availableTimes: "!bg-[#D7CEF5]",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<h1>{eventTypeSlug}</h1>
|
||||
<Booker
|
||||
eventSlug={eventTypeSlug}
|
||||
username={props.calUsername ?? ""}
|
||||
onCreateBookingSuccess={(data) => {
|
||||
setBookingTitle(data.data.title ?? "");
|
||||
router.push(`/${data.data.uid}`);
|
||||
}}
|
||||
teamId={teams?.[0]?.id || 0}
|
||||
isTeamEvent={isTeamEvent}
|
||||
duration={eventTypeDuration}
|
||||
customClassNames={{
|
||||
bookerContainer: "!bg-[#F5F2FE] [&_button:!rounded-full] border-subtle border",
|
||||
datePickerCustomClassNames: {
|
||||
datePickerDatesActive: "!bg-[#D7CEF5]",
|
||||
},
|
||||
eventMetaCustomClassNames: {
|
||||
eventMetaTitle: "text-[#7151DC]",
|
||||
},
|
||||
availableTimeSlotsCustomClassNames: {
|
||||
availableTimeSlotsHeaderContainer: "!bg-[#F5F2FE]",
|
||||
availableTimes: "!bg-[#D7CEF5]",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!bookingTitle && rescheduleUid && eventTypeSlugQueryParam && (
|
||||
<Booker
|
||||
|
||||
+6
@@ -15,6 +15,12 @@ export class GetEventTypesQuery_2024_06_14 {
|
||||
usernames?: string[];
|
||||
}
|
||||
|
||||
export class GetTeamEventTypesQuery_2024_06_14 {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
eventSlug?: string;
|
||||
}
|
||||
|
||||
function TransformUsernames() {
|
||||
return Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
|
||||
+8
-3
@@ -12,7 +12,7 @@ import {
|
||||
} from "class-validator";
|
||||
|
||||
import type { Location_2024_06_14, BookingField_2024_06_14 } from "../inputs";
|
||||
import { Host } from "../inputs";
|
||||
import { Host as TeamEventTypeHostInput } from "../inputs";
|
||||
import { RecurringEvent_2024_06_14 } from "../inputs";
|
||||
import { ValidateBookingFields_2024_06_14 } from "../inputs/booking-fields.input";
|
||||
import { ValidateLocations_2024_06_14 } from "../inputs/locations.input";
|
||||
@@ -139,6 +139,11 @@ export class EventTypeOutput_2024_06_14 {
|
||||
scheduleId!: number | null;
|
||||
}
|
||||
|
||||
export class TeamEventTypeResponseHost extends TeamEventTypeHostInput {
|
||||
@IsString()
|
||||
name!: string;
|
||||
}
|
||||
|
||||
export class TeamEventTypeOutput_2024_06_14 {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
@@ -235,9 +240,9 @@ export class TeamEventTypeOutput_2024_06_14 {
|
||||
parentEventTypeId?: number | null;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Host)
|
||||
@Type(() => TeamEventTypeResponseHost)
|
||||
@IsArray()
|
||||
hosts!: Host[];
|
||||
hosts!: TeamEventTypeResponseHost[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./bookings";
|
||||
export * from "./billing";
|
||||
export * from "./schedules";
|
||||
export * from "./event-types";
|
||||
export * from "./organizations";
|
||||
|
||||
@@ -46,6 +46,7 @@ export const userSchemaResponse = z.object({
|
||||
weekStart: z.string(),
|
||||
timeZone: z.string().default("Europe/London"),
|
||||
username: z.string(),
|
||||
organizationId: z.number().nullable(),
|
||||
});
|
||||
|
||||
export type UserResponse = z.infer<typeof userSchemaResponse>;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./teams";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./outputs";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./team.output";
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
|
||||
export class OrgTeamOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
readonly parentId?: number;
|
||||
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@Expose()
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly isOrganization?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly hideBookATeamMember?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly metadata?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly weekStart?: string = "Sunday";
|
||||
}
|
||||
Reference in New Issue
Block a user