refactor: v2 team event type assignAllTeamMembers (#20400)
* fix: platform seed data * fix: assignAllTeamMembers: true excludes team owners in platform * refactor: platform plan guard explicit error message * refactor: require either hosts or assignAllTeamMembers=true * refactor: require either hosts or assignAllTeamMembers=true * fix: before creating or updating team event type check if doesnt exist with that slug * chore: regenerate docs * test: can update round robin that had assign all team members true * fix: platform guard spec test * fix: seed platform membership role * fix: test ts error
This commit is contained in:
@@ -2,6 +2,7 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ForbiddenException } from "@nestjs/common";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
@@ -47,11 +48,11 @@ describe("PlatformPlanGuard", () => {
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the organization does not exist", async () => {
|
||||
it("should throw ForbiddenException 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);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should return true if the organization is not platform", async () => {
|
||||
@@ -66,7 +67,7 @@ describe("PlatformPlanGuard", () => {
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the organization has no subscription", async () => {
|
||||
it("should throw ForbiddenException if the organization has no subscription", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: true,
|
||||
@@ -78,10 +79,10 @@ describe("PlatformPlanGuard", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should return false if the user has a lower plan than required", async () => {
|
||||
it("should throw ForbiddenException if the user has a lower plan than required", async () => {
|
||||
jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS");
|
||||
jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({
|
||||
isPlatform: true,
|
||||
@@ -93,7 +94,7 @@ describe("PlatformPlanGuard", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should return true if the result is cached in Redis", async () => {
|
||||
@@ -103,11 +104,11 @@ describe("PlatformPlanGuard", () => {
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the result is cached in Redis", async () => {
|
||||
it("should throw ForbiddenException if the result is cached as false 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);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(context: Record<string, string | object>): ExecutionContext {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -55,7 +55,12 @@ export class PlatformPlanGuard implements CanActivate {
|
||||
}
|
||||
|
||||
await this.redisService.redis.set(REDIS_CACHE_KEY, String(canAccess), "EX", 300);
|
||||
return canAccess;
|
||||
if (canAccess) {
|
||||
return canAccess;
|
||||
}
|
||||
throw new ForbiddenException(
|
||||
`Platform plan - you do not have required plan for this operation. Minimum plan is ${minimumPlan}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import {
|
||||
GetBookingsDataEntry,
|
||||
GetBookingsOutput_2024_04_15,
|
||||
} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
|
||||
import { OrgTeamMembershipOutputDto } from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
Host,
|
||||
OrgTeamOutputDto,
|
||||
TeamEventTypeOutput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
|
||||
describe("Assign all team members", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `assign-all-team-members-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
|
||||
let managedTeam: OrgTeamOutputDto;
|
||||
|
||||
const managedUsersTimeZone = "Europe/Rome";
|
||||
const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`;
|
||||
const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`;
|
||||
let firstManagedUser: CreateManagedUserData;
|
||||
let secondManagedUser: CreateManagedUserData;
|
||||
|
||||
let roundRobinEventType: TeamEventTypeOutput_2024_06_14;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "ESSENTIALS",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: randomString(),
|
||||
username: platformAdminEmail,
|
||||
organization: { connect: { id: organization.id } },
|
||||
user: {
|
||||
connect: { id: platformAdmin.id },
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformAdmin.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 1023,
|
||||
areDefaultEventTypesEnabled: false,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("setup managed users", () => {
|
||||
it("should create first managed user", async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: firstManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
firstManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it("should create second managed user", async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: secondManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Bob Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
secondManagedUser = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
describe("should setup managed team", () => {
|
||||
it("should create managed team", async () => {
|
||||
const body: CreateOrgTeamDto = {
|
||||
name: `team-${randomString()}`,
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<OrgTeamOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
managedTeam = responseBody.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("should setup memberships", () => {
|
||||
it("should create first user's membership of the org's team", async () => {
|
||||
const body: CreateOrgTeamMembershipDto = {
|
||||
userId: firstManagedUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create second user's membership of the org's team", async () => {
|
||||
const body: CreateOrgTeamMembershipDto = {
|
||||
userId: secondManagedUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("should setup event types using assignAllTeamMembers true", () => {
|
||||
it("should not be able to setup team event type if no hosts nor assignAllTeamMembers provided", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Coding consultation round robin",
|
||||
slug: `organizations-event-types-round-robin-${randomString()}`,
|
||||
lengthInMinutes: 60,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
schedulingType: "collective",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should setup collective event type assignAllTeamMembers true", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Coding consultation collective",
|
||||
slug: `assign-all-team-members-collective-${randomString()}`,
|
||||
lengthInMinutes: 60,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
schedulingType: "collective",
|
||||
assignAllTeamMembers: true,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("collective");
|
||||
evaluateHost({ userId: firstManagedUser.user.id }, data.hosts[0]);
|
||||
evaluateHost({ userId: secondManagedUser.user.id }, data.hosts[1]);
|
||||
|
||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||
expect(eventTypeHosts.length).toEqual(2);
|
||||
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||
expect(firstHost).toBeDefined();
|
||||
expect(secondHost).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should setup round robin event type assignAllTeamMembers true", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Coding consultation round robin",
|
||||
slug: `assign-all-team-members-round-robin-${randomString()}`,
|
||||
lengthInMinutes: 60,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
schedulingType: "roundRobin",
|
||||
assignAllTeamMembers: true,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("roundRobin");
|
||||
evaluateHost(
|
||||
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||
data.hosts[0]
|
||||
);
|
||||
evaluateHost(
|
||||
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||
data.hosts[1]
|
||||
);
|
||||
|
||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||
expect(eventTypeHosts.length).toEqual(2);
|
||||
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||
expect(firstHost).toBeDefined();
|
||||
expect(secondHost).toBeDefined();
|
||||
roundRobinEventType = data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should update round robin event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Coding consultation round robin updated",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(
|
||||
`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types/${roundRobinEventType.id}`
|
||||
)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("roundRobin");
|
||||
evaluateHost(
|
||||
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||
data.hosts[0]
|
||||
);
|
||||
evaluateHost(
|
||||
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||
data.hosts[1]
|
||||
);
|
||||
|
||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||
expect(eventTypeHosts.length).toEqual(2);
|
||||
const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||
const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||
expect(firstHost).toBeDefined();
|
||||
expect(secondHost).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should setup managed event type assignAllTeamMembers true", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Coding consultation managed",
|
||||
slug: `assign-all-team-members-managed-${randomString()}`,
|
||||
lengthInMinutes: 60,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
schedulingType: "managed",
|
||||
assignAllTeamMembers: true,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`)
|
||||
.send(body)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
|
||||
const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(
|
||||
firstManagedUser.user.id
|
||||
);
|
||||
const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(
|
||||
secondManagedUser.user.id
|
||||
);
|
||||
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(managedTeam.id);
|
||||
const managedTeamEventTypes = teamEventTypes.filter(
|
||||
(eventType) => eventType.schedulingType === "MANAGED"
|
||||
);
|
||||
|
||||
expect(teammate1EventTypes.length).toEqual(1);
|
||||
expect(teammate2EventTypes.length).toEqual(1);
|
||||
expect(managedTeamEventTypes.length).toEqual(1);
|
||||
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
|
||||
|
||||
const responseTeamEvent = responseBody.data.find(
|
||||
(eventType) => eventType.schedulingType === "managed"
|
||||
);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.teamId).toEqual(managedTeam.id);
|
||||
expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true);
|
||||
|
||||
const responseTeammate1Event = responseBody.data.find(
|
||||
(eventType) => eventType.ownerId === firstManagedUser.user.id
|
||||
);
|
||||
expect(responseTeammate1Event).toBeDefined();
|
||||
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
|
||||
const responseTeammate2Event = responseBody.data.find(
|
||||
(eventType) => eventType.ownerId === secondManagedUser.user.id
|
||||
);
|
||||
expect(responseTeammate2Event).toBeDefined();
|
||||
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function evaluateHost(expected: Host, received: Host | undefined) {
|
||||
expect(expected.userId).toEqual(received?.userId);
|
||||
expect(expected.mandatory).toEqual(received?.mandatory);
|
||||
expect(expected.priority).toEqual(received?.priority);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -325,8 +325,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("collective");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
|
||||
evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
@@ -1046,8 +1046,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("roundRobin");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
|
||||
evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
|
||||
@@ -33,6 +33,7 @@ export class InputOrganizationsEventTypesService {
|
||||
inputEventType: CreateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
await this.validateHosts(teamId, inputEventType.hosts);
|
||||
await this.validateTeamEventTypeSlug(teamId, inputEventType.slug);
|
||||
|
||||
const transformedBody = await this.transformInputCreateTeamEventType(teamId, inputEventType);
|
||||
|
||||
@@ -62,6 +63,9 @@ export class InputOrganizationsEventTypesService {
|
||||
inputEventType: UpdateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
await this.validateHosts(teamId, inputEventType.hosts);
|
||||
if (inputEventType.slug) {
|
||||
await this.validateTeamEventTypeSlug(teamId, inputEventType.slug);
|
||||
}
|
||||
|
||||
const transformedBody = await this.transformInputUpdateTeamEventType(eventTypeId, teamId, inputEventType);
|
||||
|
||||
@@ -85,10 +89,28 @@ export class InputOrganizationsEventTypesService {
|
||||
return transformedBody;
|
||||
}
|
||||
|
||||
async validateTeamEventTypeSlug(teamId: number, slug: string) {
|
||||
const teamEventWithSlugExists = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(
|
||||
teamId,
|
||||
slug
|
||||
);
|
||||
|
||||
if (teamEventWithSlugExists) {
|
||||
throw new BadRequestException("Team event type with this slug already exists");
|
||||
}
|
||||
}
|
||||
|
||||
async transformInputCreateTeamEventType(
|
||||
teamId: number,
|
||||
inputEventType: CreateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
const hasHosts = !!inputEventType.hosts && !!inputEventType.hosts.length;
|
||||
const hasAssignAllTeamMembers = inputEventType.assignAllTeamMembers === true;
|
||||
|
||||
if (!hasHosts && !hasAssignAllTeamMembers) {
|
||||
throw new BadRequestException("Either hosts must be provided or assignAllTeamMembers must be true");
|
||||
}
|
||||
|
||||
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
|
||||
|
||||
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
|
||||
@@ -185,7 +207,7 @@ export class InputOrganizationsEventTypesService {
|
||||
eventType: { children: { userId: number | null }[] } | null
|
||||
) {
|
||||
if (inputEventType.assignAllTeamMembers) {
|
||||
return await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
return await this.getTeamUsersIds(teamId);
|
||||
}
|
||||
|
||||
// note(Lauris): when API user updates managed event type users
|
||||
@@ -197,6 +219,17 @@ export class InputOrganizationsEventTypesService {
|
||||
return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[];
|
||||
}
|
||||
|
||||
async getTeamUsersIds(teamId: number) {
|
||||
const team = await this.teamsRepository.getById(teamId);
|
||||
const isPlatformTeam = !!team?.createdByOAuthClientId;
|
||||
if (isPlatformTeam) {
|
||||
// note(Lauris): platform team creators have role "OWNER" but we don't want to assign them to team members marked as "assignAllTeamMembers: true"
|
||||
// because they are not a managed user.
|
||||
return await this.teamsRepository.getTeamManagedUsersIds(teamId);
|
||||
}
|
||||
return await this.teamsRepository.getTeamUsersIds(teamId);
|
||||
}
|
||||
|
||||
transformInputTeamLocations(inputLocations: CreateTeamEventTypeInput_2024_06_14["locations"]) {
|
||||
return transformTeamLocationsApiToInternal(inputLocations);
|
||||
}
|
||||
@@ -217,7 +250,7 @@ export class InputOrganizationsEventTypesService {
|
||||
}
|
||||
|
||||
async getAllTeamMembers(teamId: number, schedulingType: SchedulingType | null) {
|
||||
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
const membersIds = await this.getTeamUsersIds(teamId);
|
||||
const isFixed = schedulingType === "COLLECTIVE" ? true : false;
|
||||
|
||||
return membersIds.map((id) => ({
|
||||
@@ -249,7 +282,7 @@ export class InputOrganizationsEventTypesService {
|
||||
|
||||
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
|
||||
if (hosts && hosts.length) {
|
||||
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
const membersIds = await this.getTeamUsersIds(teamId);
|
||||
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
|
||||
if (invalidHosts.length) {
|
||||
throw new NotFoundException(
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@ export class CreateOrgMembershipDto {
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
@ApiProperty({
|
||||
enum: ["MEMBER", "OWNER", "ADMIN"],
|
||||
description: "If you are platform customer then managed users should only have MEMBER role.",
|
||||
})
|
||||
readonly role: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+2
-2
@@ -304,8 +304,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("collective");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]);
|
||||
evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
|
||||
@@ -49,6 +49,17 @@ export class TeamsEventTypesRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeByTeamIdAndSlug(teamId: number, eventTypeSlug: string) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: {
|
||||
teamId_slug: {
|
||||
teamId,
|
||||
slug: eventTypeSlug,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamEventTypes(teamId: number) {
|
||||
return this.dbRead.prisma.eventType.findMany({
|
||||
where: {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TeamsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamMembersIds(teamId: number) {
|
||||
async getTeamUsersIds(teamId: number) {
|
||||
const teamMembers = await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
@@ -43,6 +43,22 @@ export class TeamsRepository {
|
||||
return teamMembers.map((member) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamManagedUsersIds(teamId: number) {
|
||||
const teamMembers = await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
user: {
|
||||
isPlatformManaged: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!teamMembers || teamMembers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamsUserIsMemberOf(userId: number) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
|
||||
@@ -12818,7 +12818,8 @@
|
||||
"MEMBER",
|
||||
"OWNER",
|
||||
"ADMIN"
|
||||
]
|
||||
],
|
||||
"description": "If you are platform customer then managed users should only have MEMBER role."
|
||||
},
|
||||
"disableImpersonation": {
|
||||
"type": "boolean",
|
||||
@@ -13202,6 +13203,7 @@
|
||||
"description": "The scheduling type for the team event - collective, roundRobin or managed."
|
||||
},
|
||||
"hosts": {
|
||||
"description": "Hosts contain specific team members you want to assign to this event type, but if you want to assign all team members, use `assignAllTeamMembers: true` instead and omit this field. For platform customers the hosts can include userIds only of managed users.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
@@ -13248,8 +13250,7 @@
|
||||
"lengthInMinutes",
|
||||
"title",
|
||||
"slug",
|
||||
"schedulingType",
|
||||
"hosts"
|
||||
"schedulingType"
|
||||
]
|
||||
},
|
||||
"CreateTeamEventTypeOutput": {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { EventType } from "@prisma/client";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@@ -22,4 +20,8 @@ export class HostsRepositoryFixture {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeHosts(eventTypeId: number) {
|
||||
return this.prismaReadClient.host.findMany({ where: { eventTypeId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11699,7 +11699,8 @@
|
||||
"role": {
|
||||
"type": "string",
|
||||
"default": "MEMBER",
|
||||
"enum": ["MEMBER", "OWNER", "ADMIN"]
|
||||
"enum": ["MEMBER", "OWNER", "ADMIN"],
|
||||
"description": "If you are platform customer then managed users should only have MEMBER role."
|
||||
},
|
||||
"disableImpersonation": {
|
||||
"type": "boolean",
|
||||
@@ -12036,6 +12037,7 @@
|
||||
"description": "The scheduling type for the team event - collective, roundRobin or managed."
|
||||
},
|
||||
"hosts": {
|
||||
"description": "Hosts contain specific team members you want to assign to this event type, but if you want to assign all team members, use `assignAllTeamMembers: true` instead and omit this field. For platform customers the hosts can include userIds only of managed users.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
@@ -12078,7 +12080,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["lengthInMinutes", "title", "slug", "schedulingType", "hosts"]
|
||||
"required": ["lengthInMinutes", "title", "slug", "schedulingType"]
|
||||
},
|
||||
"CreateTeamEventTypeOutput": {
|
||||
"type": "object",
|
||||
|
||||
+35
-2
@@ -5,6 +5,7 @@ import {
|
||||
ApiExtraModels,
|
||||
} from "@nestjs/swagger";
|
||||
import { Type, Transform, Expose } from "class-transformer";
|
||||
import type { ValidationOptions, ValidationArguments } from "class-validator";
|
||||
import {
|
||||
IsString,
|
||||
IsInt,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
ValidateNested,
|
||||
ArrayNotEmpty,
|
||||
ArrayUnique,
|
||||
registerDecorator,
|
||||
} from "class-validator";
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-enums";
|
||||
@@ -446,6 +448,32 @@ export class Host {
|
||||
priority?: keyof typeof HostPriority = "medium";
|
||||
}
|
||||
|
||||
function RequireHostsOrAssignAllMembers(validationOptions?: ValidationOptions) {
|
||||
return function (object: any) {
|
||||
registerDecorator({
|
||||
name: "requireHostsOrAssignAllMembers",
|
||||
target: object,
|
||||
propertyName: "hosts OR assignAllTeamMembers",
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: {
|
||||
validate(_value: any, args: ValidationArguments) {
|
||||
const obj = args.object as CreateTeamEventTypeInput_2024_06_14;
|
||||
|
||||
const hasHosts = !!obj.hosts && !!obj.hosts.length;
|
||||
const hasAssignAllTeamMembers = obj.assignAllTeamMembers === true;
|
||||
|
||||
return hasHosts || hasAssignAllTeamMembers;
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return "Either hosts OR assignAllTeamMembers set to true is required";
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@RequireHostsOrAssignAllMembers()
|
||||
export class CreateTeamEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput {
|
||||
@Transform(({ value }) => {
|
||||
if (value === "collective") {
|
||||
@@ -470,8 +498,13 @@ export class CreateTeamEventTypeInput_2024_06_14 extends BaseCreateEventTypeInpu
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Host)
|
||||
@IsArray()
|
||||
@DocsProperty({ type: [Host] })
|
||||
hosts!: Host[];
|
||||
@IsOptional()
|
||||
@DocsPropertyOptional({
|
||||
type: [Host],
|
||||
description:
|
||||
"Hosts contain specific team members you want to assign to this event type, but if you want to assign all team members, use `assignAllTeamMembers: true` instead and omit this field. For platform customers the hosts can include userIds only of managed users.",
|
||||
})
|
||||
hosts?: Host[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -163,20 +163,22 @@ async function createPlatformAndSetupUser({
|
||||
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
|
||||
);
|
||||
|
||||
const { role = MembershipRole.OWNER, username } = platformUser;
|
||||
const { username } = platformUser;
|
||||
|
||||
const membershipRole = MembershipRole.OWNER;
|
||||
|
||||
if (!!team) {
|
||||
await associateUserAndOrg({
|
||||
teamId: team.id,
|
||||
userId: platformUser.id,
|
||||
role: role as MembershipRole,
|
||||
role: membershipRole,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
await prisma.platformBilling.create({
|
||||
data: {
|
||||
id: team?.id,
|
||||
plan: "STARTER",
|
||||
plan: "SCALE",
|
||||
customerId: "cus_123",
|
||||
subscriptionId: "sub_123",
|
||||
},
|
||||
@@ -194,7 +196,7 @@ async function createPlatformAndSetupUser({
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWNtZSAiLCJwZXJtaXNzaW9ucyI6MTAyMywicmVkaXJlY3RVcmlzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDMyMSJdLCJib29raW5nUmVkaXJlY3RVcmkiOiIiLCJib29raW5nQ2FuY2VsUmVkaXJlY3RVcmkiOiIiLCJib29raW5nUmVzY2hlZHVsZVJlZGlyZWN0VXJpIjoiIiwiYXJlRW1haWxzRW5hYmxlZCI6dHJ1ZSwiaWF0IjoxNzE5NTk1ODA4fQ.L5_jSS14fcKLCD_9_DAOgtGd6lUSZlU5CEpCPaPt41I",
|
||||
},
|
||||
});
|
||||
console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${role}'`);
|
||||
console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${membershipRole}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,7 +963,7 @@ async function main() {
|
||||
password: "PLATFORMadmin2024!",
|
||||
username: "platform",
|
||||
name: "Platform Admin",
|
||||
role: "ADMIN",
|
||||
role: "USER",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user