feat: V2 Org Memberships (#15578)
* init * --WIP * WIP * ... * more wip * doc * address feedback --WIP * further requested changes * fix * add missing parseIntPipe * chore: code review comments + e2e tests * fixup! chore: code review comments + e2e tests * fixup! fixup! chore: code review comments + e2e tests * fix: don't allow passing teamId for org membership create/update * fix: improve docs and DTOs --------- Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
1 parent
af3d2e6879
commit
22fc47f69f
17 files changed
+1482
-260
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
import { Membership } from "@calcom/prisma/client";
|
||||
|
||||
export type GetMembershipReturnType = Membership;
|
||||
|
||||
export const GetMembership = createParamDecorator<
|
||||
keyof GetMembershipReturnType | (keyof GetMembershipReturnType)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const membership = request.membership as GetMembershipReturnType;
|
||||
|
||||
if (!membership) {
|
||||
throw new Error("GetMembership decorator : Membership not found");
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
...prev,
|
||||
[curr]: membership[curr],
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return membership[data];
|
||||
}
|
||||
|
||||
return membership;
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Membership } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsMembershipInOrg implements CanActivate {
|
||||
constructor(private organizationsMembershipRepository: OrganizationsMembershipRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { membership: Membership }>();
|
||||
const membershipId: string = request.params.membershipId;
|
||||
const orgId: string = request.params.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
throw new ForbiddenException("No org id found in request params.");
|
||||
}
|
||||
|
||||
if (!membershipId) {
|
||||
throw new ForbiddenException("No membership id found in request params.");
|
||||
}
|
||||
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
Number(orgId),
|
||||
Number(membershipId)
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException(`Membership (${membershipId}) not found.`);
|
||||
}
|
||||
|
||||
request.membership = membership;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import { Membership, Team } from "@calcom/prisma/client";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../../inputs/update-organization-membership.input";
|
||||
|
||||
describe("Organizations Memberships Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let membership: Membership;
|
||||
let membership2: Membership;
|
||||
let membershipCreatedViaApi: Membership;
|
||||
|
||||
const userEmail = "org-admin-membership-controller-e2e@api.com";
|
||||
const userEmail2 = "org-member-membership-controller-e2e@api.com";
|
||||
|
||||
const invitedUserEmail = "org-member-invited-membership-controller-e2e@api.com";
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
|
||||
let userToInviteViaApi: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userToInviteViaApi = await userRepositoryFixture.create({
|
||||
email: invitedUserEmail,
|
||||
username: invitedUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
membership = await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
membership2 = await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should get all the memberships of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership.id);
|
||||
expect(responseBody.data[1].id).toEqual(membership2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all the memberships of the org paginated", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership2.id);
|
||||
expect(responseBody.data[0].userId).toEqual(user2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail if org does not exist", async () => {
|
||||
return request(app.getHttpServer()).get(`/v2/organizations/120494059/memberships`).expect(403);
|
||||
});
|
||||
|
||||
it("should get the membership of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships/${membership.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membership.id);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create the membership of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.send({
|
||||
userId: userToInviteViaApi.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
} satisfies CreateOrgMembershipDto)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.teamId).toEqual(org.id);
|
||||
expect(membershipCreatedViaApi.role).toEqual("MEMBER");
|
||||
expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update the membership of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
.send({
|
||||
role: "OWNER",
|
||||
} satisfies UpdateOrgMembershipDto)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.role).toEqual("OWNER");
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete the membership of the org we created via api", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to get the membership of the org we just deleted", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should fail if the membership does not exist", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships/123132145`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organizations Memberships Endpoints", () => {
|
||||
describe("User Authentication - User is Org Member", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let membership: Membership;
|
||||
|
||||
const userEmail = "org-member-memberships-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
membership = await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should deny get all the memberships of the org", async () => {
|
||||
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/memberships`).expect(403);
|
||||
});
|
||||
|
||||
it("should deny get all the memberships of the org paginated", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should deny get the membership of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/memberships/${membership.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should deny create the membership for the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/memberships`)
|
||||
.send({
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
} satisfies CreateOrgMembershipDto)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should deny update the membership of the org", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/memberships/${membership.id}`)
|
||||
.send({
|
||||
role: "MEMBER",
|
||||
} satisfies Partial<CreateOrgMembershipDto>)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should deny delete the membership of the org we created via api", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/memberships/${membership.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
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 { 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";
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { UpdateOrgMembershipDto } from "@/modules/organizations/inputs/update-organization-membership.input";
|
||||
import { CreateOrgMembershipOutput } from "@/modules/organizations/outputs/organization-membership/create-membership.output";
|
||||
import { DeleteOrgMembership } from "@/modules/organizations/outputs/organization-membership/delete-membership.output";
|
||||
import { GetAllOrgMemberships } from "@/modules/organizations/outputs/organization-membership/get-all-memberships.output";
|
||||
import { GetOrgMembership } from "@/modules/organizations/outputs/organization-membership/get-membership.output";
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { UpdateOrgMembership } from "@/modules/organizations/outputs/organization-membership/update-membership.output";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
Delete,
|
||||
Patch,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
import { Membership } from "@calcom/prisma/client";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/memberships",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
|
||||
@DocsTags("Organizations Memberships")
|
||||
export class OrganizationsMembershipsController {
|
||||
constructor(private organizationsMembershipService: OrganizationsMembershipService) {}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getAllMemberships(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetAllOrgMemberships> {
|
||||
const { skip, take } = queryParams;
|
||||
const memberships = await this.organizationsMembershipService.getPaginatedOrgMemberships(
|
||||
orgId,
|
||||
skip ?? 0,
|
||||
take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: memberships.map((membership) =>
|
||||
plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" })
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async createMembership(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Body() body: CreateOrgMembershipDto
|
||||
): Promise<CreateOrgMembershipOutput> {
|
||||
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Get("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getUserSchedule(@GetMembership() membership: Membership): Promise<GetOrgMembership> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Delete("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async deleteMembership(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Param("membershipId", ParseIntPipe) membershipId: number
|
||||
): Promise<DeleteOrgMembership> {
|
||||
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(IsMembershipInOrg)
|
||||
@Roles("ORG_ADMIN")
|
||||
@Patch("/:membershipId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateMembership(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Param("membershipId", ParseIntPipe) membershipId: number,
|
||||
@Body() body: UpdateOrgMembershipDto
|
||||
): Promise<UpdateOrgMembership> {
|
||||
const membership = await this.organizationsMembershipService.updateOrgMembership(
|
||||
orgId,
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
export class CreateOrgMembershipDto {
|
||||
@IsInt()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
export class UpdateOrgMembershipDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
}
|
||||
@@ -2,13 +2,16 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
|
||||
@@ -32,6 +35,8 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
EmailService,
|
||||
OrganizationsMembershipRepository,
|
||||
OrganizationsMembershipService,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
@@ -39,7 +44,14 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsRepository,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
OrganizationsMembershipRepository,
|
||||
OrganizationsMembershipService,
|
||||
],
|
||||
controllers: [
|
||||
OrganizationsTeamsController,
|
||||
OrganizationsSchedulesController,
|
||||
OrganizationsUsersController,
|
||||
OrganizationsMembershipsController,
|
||||
],
|
||||
controllers: [OrganizationsTeamsController, OrganizationsSchedulesController, OrganizationsUsersController],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CreateOrgMembershipOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: OrgMembershipOutputDto,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => OrgMembershipOutputDto)
|
||||
data!: OrgMembershipOutputDto;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class DeleteOrgMembership {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
data!: OrgMembershipOutputDto;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetAllOrgMemberships {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: OrgMembershipOutputDto,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OrgMembershipOutputDto)
|
||||
@IsArray()
|
||||
data!: OrgMembershipOutputDto[];
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetOrgMembership {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: OrgMembershipOutputDto,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => OrgMembershipOutputDto)
|
||||
data!: OrgMembershipOutputDto;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class OrgMembershipOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly teamId!: number;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly accepted!: boolean;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
@Expose()
|
||||
readonly role!: MembershipRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { OrgMembershipOutputDto } from "@/modules/organizations/outputs/organization-membership/membership.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateOrgMembership {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: OrgMembershipOutputDto,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => OrgMembershipOutputDto)
|
||||
data!: OrgMembershipOutputDto;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsMembershipRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async findOrgMembership(organizationId: number, membershipId: number) {
|
||||
return this.dbRead.prisma.membership.findUnique({
|
||||
where: {
|
||||
id: membershipId,
|
||||
teamId: organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
return this.dbWrite.prisma.membership.delete({
|
||||
where: {
|
||||
id: membershipId,
|
||||
teamId: organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: organizationId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.update({
|
||||
data: { ...data },
|
||||
where: { id: membershipId, teamId: organizationId },
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgMembershipsPaginated(organizationId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsMembershipService {
|
||||
constructor(
|
||||
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository
|
||||
) {}
|
||||
|
||||
async getOrgMembership(organizationId: number, membershipId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
||||
const memberships = await this.organizationsMembershipRepository.findOrgMembershipsPaginated(
|
||||
organizationId,
|
||||
skip,
|
||||
take
|
||||
);
|
||||
return memberships;
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
const memberships = await this.organizationsMembershipRepository.deleteOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
return memberships;
|
||||
}
|
||||
|
||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||
const membership = await this.organizationsMembershipRepository.updateOrgMembership(
|
||||
organizationId,
|
||||
membershipId,
|
||||
data
|
||||
);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
||||
return membership;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -353,7 +353,6 @@ model Profile {
|
||||
@@unique([userId, organizationId])
|
||||
// Allow username reuse only across different organizations
|
||||
@@unique([username, organizationId])
|
||||
|
||||
@@index([uid])
|
||||
@@index([userId])
|
||||
@@index([organizationId])
|
||||
|
||||
Reference in new issue
Block a user