chore: v2 dont allow mixing platform teams/users with non-platform (#20296)
* chore: v2 dont allow mixing platform teams/users with non-platform * chore: finish main merge * test: e2e * refactor: simplify can add user to team logic * refactor: fetch only needed oauth data * refactor: cleanup
This commit is contained in:
@@ -119,6 +119,15 @@ export class OAuthClientRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getByOrgId(organizationId: number) {
|
||||
return this.dbRead.prisma.platformOAuthClient.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getByEventTypeHosts(eventTypeId: number) {
|
||||
const hostWithUserPlatformClient = await this.dbRead.prisma.host.findFirst({
|
||||
select: {
|
||||
|
||||
+34
-2
@@ -1,17 +1,23 @@
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR = `Can't add user to organization - the user is platform managed user but organization is not because organization probably was not created using OAuth credentials.`;
|
||||
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR = `Can't add user to organization - the user is not platform managed user but organization is platform managed. Both have to be created using OAuth credentials.`;
|
||||
export const MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to organization - managed user and organization were created using different OAuth clients.`;
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsMembershipService {
|
||||
constructor(
|
||||
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
|
||||
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
|
||||
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService,
|
||||
private readonly oAuthClientsRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async getOrgMembership(organizationId: number, membershipId: number) {
|
||||
@@ -95,7 +101,33 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
await this.canUserBeAddedToOrg(data.userId, organizationId);
|
||||
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
async canUserBeAddedToOrg(userId: number, orgId: number) {
|
||||
const [userOAuthClient, orgOAuthClients] = await Promise.all([
|
||||
this.oAuthClientsRepository.getByUserId(userId),
|
||||
this.oAuthClientsRepository.getByOrgId(orgId),
|
||||
]);
|
||||
|
||||
if (!userOAuthClient && orgOAuthClients.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userOAuthClient && orgOAuthClients.some((orgClient) => orgClient.id === userOAuthClient.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!userOAuthClient) {
|
||||
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
|
||||
}
|
||||
|
||||
if (orgOAuthClients.length === 0) {
|
||||
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
|
||||
}
|
||||
|
||||
throw new BadRequestException(MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.ser
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/attributes/index/controllers/organizations-attributes.controller";
|
||||
@@ -65,6 +66,8 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -93,6 +96,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsOrganizationsModule,
|
||||
OrganizationsStripeModule,
|
||||
OrganizationsTeamsRoutingFormsModule,
|
||||
MembershipsModule,
|
||||
OrganizationsConferencingModule,
|
||||
EventTypesPrivateLinksModule,
|
||||
],
|
||||
@@ -101,7 +105,6 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsRepository,
|
||||
OrganizationsService,
|
||||
OrganizationsTeamsService,
|
||||
MembershipsRepository,
|
||||
OrganizationsSchedulesService,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
@@ -147,6 +150,9 @@ import { Module } from "@nestjs/common";
|
||||
TeamsSchedulesService,
|
||||
SchedulesService_2024_06_11,
|
||||
InputSchedulesService_2024_06_11,
|
||||
TeamsMembershipsService,
|
||||
TeamsMembershipsRepository,
|
||||
OAuthClientRepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import {
|
||||
PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR,
|
||||
REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR,
|
||||
} from "@/modules/organizations/memberships/services/organizations-membership.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 { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import {
|
||||
PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR,
|
||||
REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR,
|
||||
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR,
|
||||
} from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { CreateUserInput } from "@/modules/users/inputs/create-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, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let orgOwner: User;
|
||||
let orgUser: User;
|
||||
let orgTeam: Team;
|
||||
let orgApiKey: string;
|
||||
|
||||
let platformOrg: Team;
|
||||
let platformOrgOwner: User;
|
||||
let platformOrgUser: CreateManagedUserData;
|
||||
let platformOrgTeam: Team;
|
||||
let platformOAuthClient: PlatformOAuthClient;
|
||||
let secondPlatformOAuthClient: PlatformOAuthClient;
|
||||
let secondPlatformOrgUser: CreateManagedUserData;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, TeamsMembershipsModule],
|
||||
}).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
|
||||
const orgName = `organizations-teams-memberships-organization-${randomString()}`;
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: orgName,
|
||||
slug: orgName,
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
const platformOrgName = `organizations-teams-memberships-platform-organization-${randomString()}`;
|
||||
platformOrg = await organizationsRepositoryFixture.create({
|
||||
isPlatform: true,
|
||||
name: platformOrgName,
|
||||
slug: platformOrgName,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "ESSENTIALS",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
platformOAuthClient = await createOAuthClient(platformOrg.id);
|
||||
secondPlatformOAuthClient = await createOAuthClient(platformOrg.id);
|
||||
|
||||
orgOwner = await userRepositoryFixture.create({
|
||||
email: `organizations-teams-memberships-org-owner-${randomString()}@api.com`,
|
||||
username: `organizations-teams-memberships-org-owner-${randomString()}`,
|
||||
});
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(orgOwner.id, null);
|
||||
orgApiKey = `cal_test_${keyString}`;
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: orgOwner.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
platformOrgOwner = await userRepositoryFixture.create({
|
||||
email: `platform-org-owner-${randomString()}@api.com`,
|
||||
username: `platform-org-owner-${randomString()}`,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformOrgOwner.id } },
|
||||
team: { connect: { id: platformOrg.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
username: `platform-org-owner-${randomString()}`,
|
||||
organization: { connect: { id: platformOrg.id } },
|
||||
user: {
|
||||
connect: { id: platformOrgOwner.id },
|
||||
},
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd-2",
|
||||
username: `org-owner-${randomString()}`,
|
||||
organization: { connect: { id: org.id } },
|
||||
user: {
|
||||
connect: { id: orgOwner.id },
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:4321"],
|
||||
permissions: 1023,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("should create org users", () => {
|
||||
it("should create a new org user", async () => {
|
||||
const newOrgUser: CreateUserInput = {
|
||||
email: `organization-user-${randomString()}@api.com`,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users`)
|
||||
.send(newOrgUser)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.email).toBe(newOrgUser.email);
|
||||
orgUser = userData;
|
||||
});
|
||||
|
||||
it(`should create a new platform org manager user using first oAuth client`, async () => {
|
||||
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: managedUserEmail,
|
||||
timeZone: "Europe/Berlin",
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
bio: "I am a bio",
|
||||
metadata: {
|
||||
key: "value",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${platformOAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
await userConnectedToOAuth(platformOAuthClient.id, responseBody.data.user.email, 1);
|
||||
platformOrgUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create a new platform org manager user using second oAuth client`, async () => {
|
||||
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: managedUserEmail,
|
||||
timeZone: "Europe/Berlin",
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
bio: "I am a bio",
|
||||
metadata: {
|
||||
key: "value",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${secondPlatformOAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", secondPlatformOAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
await userConnectedToOAuth(secondPlatformOAuthClient.id, responseBody.data.user.email, 1);
|
||||
secondPlatformOrgUser = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
describe("should create org teams", () => {
|
||||
it("should create the team for org", async () => {
|
||||
const teamName = `organization team ${randomString()}`;
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
name: teamName,
|
||||
} satisfies CreateOrgTeamDto)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Team> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
orgTeam = responseBody.data;
|
||||
expect(orgTeam.name).toEqual(teamName);
|
||||
expect(orgTeam.parentId).toEqual(org.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create the team for platform org", async () => {
|
||||
const teamName = `platform organization team ${randomString()}`;
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
name: teamName,
|
||||
} satisfies CreateOrgTeamDto)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Team> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
platformOrgTeam = responseBody.data;
|
||||
expect(platformOrgTeam.name).toEqual(teamName);
|
||||
expect(platformOrgTeam.parentId).toEqual(platformOrg.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("organization memberships", () => {
|
||||
describe("negative tests", () => {
|
||||
it("should not add managed user to organization", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
|
||||
});
|
||||
|
||||
it("should not add managed user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it("should not add managed user to organization team", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive tests", () => {
|
||||
it("should add user to organization", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should add user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.set("Authorization", `Bearer ${orgApiKey}`)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("platform organization memberships", () => {
|
||||
describe("negative tests", () => {
|
||||
it("should not add user to platform organization", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
|
||||
});
|
||||
|
||||
it("should not add user to platform organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(422);
|
||||
});
|
||||
it("should not add user to platform organization team", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: orgUser.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
|
||||
});
|
||||
|
||||
it("should not add user to platform organization team because user is created using different oAuth client", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: secondPlatformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toEqual(
|
||||
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive tests", () => {
|
||||
it("should add user to organization", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should add user to organization team", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
|
||||
.set("x-cal-client-id", platformOAuthClient.id)
|
||||
.set("x-cal-secret-key", platformOAuthClient.secret)
|
||||
.send({
|
||||
userId: platformOrgUser.user.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
|
||||
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
|
||||
|
||||
expect(oAuthUsers?.length).toEqual(usersCount);
|
||||
expect(newOAuthUser?.email).toEqual(userEmail);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await teamsRepositoryFixture.delete(orgTeam.id);
|
||||
await organizationsRepositoryFixture.delete(platformOrg.id);
|
||||
await teamsRepositoryFixture.delete(platformOrgTeam.id);
|
||||
await userRepositoryFixture.deleteByEmail(platformOrgUser.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(orgUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(orgOwner.email);
|
||||
await userRepositoryFixture.deleteByEmail(platformOrgOwner.email);
|
||||
await userRepositoryFixture.deleteByEmail(secondPlatformOrgUser.user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
|
||||
import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input";
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
@@ -8,10 +9,12 @@ import { TeamService } from "@calcom/platform-libraries";
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository
|
||||
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository,
|
||||
private readonly teamsMembershipsService: TeamsMembershipsService
|
||||
) {}
|
||||
|
||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||
await this.teamsMembershipsService.canUserBeAddedToTeam(data.userId, teamId);
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.createOrgTeamMembership(
|
||||
teamId,
|
||||
data
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR = `Can't add user to team - the user is platform managed user but team is not because team probably was not created using OAuth credentials.`;
|
||||
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR = `Can't add user to team - the user is not platform managed user but team is platform managed. Both have to be created using OAuth credentials.`;
|
||||
export const PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to team - managed user and team were created using different OAuth clients.`;
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
constructor(
|
||||
private readonly teamsMembershipsRepository: TeamsMembershipsRepository,
|
||||
private readonly oAuthClientsRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
||||
await this.canUserBeAddedToTeam(data.userId, teamId);
|
||||
const teamMembership = await this.teamsMembershipsRepository.createTeamMembership(teamId, data);
|
||||
return teamMembership;
|
||||
}
|
||||
@@ -54,4 +63,29 @@ export class TeamsMembershipsService {
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
|
||||
async canUserBeAddedToTeam(userId: number, teamId: number) {
|
||||
const [userOAuthClient, teamOAuthClient] = await Promise.all([
|
||||
this.oAuthClientsRepository.getByUserId(userId),
|
||||
this.oAuthClientsRepository.getByTeamId(teamId),
|
||||
]);
|
||||
|
||||
if (!userOAuthClient && !teamOAuthClient) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userOAuthClient && teamOAuthClient && userOAuthClient.id === teamOAuthClient.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!teamOAuthClient) {
|
||||
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
|
||||
}
|
||||
|
||||
if (!userOAuthClient) {
|
||||
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
|
||||
}
|
||||
|
||||
throw new BadRequestException(PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ import { Module } from "@nestjs/common";
|
||||
imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule, TeamsEventTypesModule],
|
||||
providers: [TeamsMembershipsRepository, TeamsMembershipsService],
|
||||
controllers: [TeamsMembershipsController],
|
||||
exports: [TeamsMembershipsService],
|
||||
})
|
||||
export class TeamsMembershipsModule {}
|
||||
|
||||
Reference in New Issue
Block a user