From 22fc47f69fb85c30cf07b5c039191f0b945a4b51 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Thu, 18 Jul 2024 07:43:20 +0400 Subject: [PATCH] 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 --- .../get-membership.decorator.ts | 33 + .../memberships/is-membership-in-org.guard.ts | 42 + ...izations-membership.controller.e2e-spec.ts | 311 ++++++ .../organizations-membership.controller.ts | 127 +++ .../create-organization-membership.input.ts | 20 + .../update-organization-membership.input.ts | 17 + .../organizations/organizations.module.ts | 14 +- .../create-membership.output.ts | 20 + .../delete-membership.output.ts | 13 + .../get-all-memberships.output.ts | 21 + .../get-membership.output.ts | 20 + .../membership.output.ts | 32 + .../update-membership.output.ts | 20 + .../organizations-membership.repository.ts | 52 + .../organizations-membership.service.ts | 53 + apps/api/v2/swagger/documentation.json | 946 +++++++++++++----- packages/prisma/schema.prisma | 1 - 17 files changed, 1482 insertions(+), 260 deletions(-) create mode 100644 apps/api/v2/src/modules/auth/decorators/get-membership/get-membership.decorator.ts create mode 100644 apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts create mode 100644 apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.e2e-spec.ts create mode 100644 apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.ts create mode 100644 apps/api/v2/src/modules/organizations/inputs/create-organization-membership.input.ts create mode 100644 apps/api/v2/src/modules/organizations/inputs/update-organization-membership.input.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/create-membership.output.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/delete-membership.output.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/get-all-memberships.output.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/get-membership.output.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/membership.output.ts create mode 100644 apps/api/v2/src/modules/organizations/outputs/organization-membership/update-membership.output.ts create mode 100644 apps/api/v2/src/modules/organizations/repositories/organizations-membership.repository.ts create mode 100644 apps/api/v2/src/modules/organizations/services/organizations-membership.service.ts diff --git a/apps/api/v2/src/modules/auth/decorators/get-membership/get-membership.decorator.ts b/apps/api/v2/src/modules/auth/decorators/get-membership/get-membership.decorator.ts new file mode 100644 index 0000000000..6dc57ed261 --- /dev/null +++ b/apps/api/v2/src/modules/auth/decorators/get-membership/get-membership.decorator.ts @@ -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; +}); diff --git a/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts new file mode 100644 index 0000000000..56a3f3c48c --- /dev/null +++ b/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts @@ -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 { + const request = context.switchToHttp().getRequest(); + 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; + } +} diff --git a/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.e2e-spec.ts new file mode 100644 index 0000000000..a0762f94ef --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.e2e-spec.ts @@ -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 = 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 = 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 = 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 = 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 = 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 = 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) + .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(); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.ts b/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.ts new file mode 100644 index 0000000000..fdf6bc0ec2 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/memberships/organizations-membership.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + const membership = await this.organizationsMembershipService.updateOrgMembership( + orgId, + membershipId, + body + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }), + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/inputs/create-organization-membership.input.ts b/apps/api/v2/src/modules/organizations/inputs/create-organization-membership.input.ts new file mode 100644 index 0000000000..b2e75985a4 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/inputs/create-organization-membership.input.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/inputs/update-organization-membership.input.ts b/apps/api/v2/src/modules/organizations/inputs/update-organization-membership.input.ts new file mode 100644 index 0000000000..ef1e75a1e0 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/inputs/update-organization-membership.input.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 0092364b84..2a83014ece 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -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 {} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/create-membership.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/create-membership.output.ts new file mode 100644 index 0000000000..5a129c9824 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/create-membership.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/delete-membership.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/delete-membership.output.ts new file mode 100644 index 0000000000..2f3005ba95 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/delete-membership.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-all-memberships.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-all-memberships.output.ts new file mode 100644 index 0000000000..3114c2c899 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-all-memberships.output.ts @@ -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[]; +} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-membership.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-membership.output.ts new file mode 100644 index 0000000000..416a3a6bb4 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/get-membership.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/membership.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/membership.output.ts new file mode 100644 index 0000000000..4557ec9457 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/membership.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-membership/update-membership.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-membership/update-membership.output.ts new file mode 100644 index 0000000000..d971822169 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/outputs/organization-membership/update-membership.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-membership.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-membership.repository.ts new file mode 100644 index 0000000000..2f2d3dd948 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-membership.repository.ts @@ -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, + }); + } +} diff --git a/apps/api/v2/src/modules/organizations/services/organizations-membership.service.ts b/apps/api/v2/src/modules/organizations/services/organizations-membership.service.ts new file mode 100644 index 0000000000..8d06878b11 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/services/organizations-membership.service.ts @@ -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; + } +} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 61f11cc6a2..82f5aacdd1 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -1085,16 +1085,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" - } - } - } - }, "responses": { "201": { "description": "", @@ -1197,16 +1187,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" - } - } - } - }, "responses": { "200": { "description": "", @@ -1271,25 +1251,47 @@ "schema": { "type": "number" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetOrganizationsUsersInput" + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "emails", + "required": false, + "in": "query", + "description": "The email address or an array of email addresses to filter by", + "schema": { + "type": "array", + "items": { + "type": "string" } } } - }, + ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GetOrganizationUsersOutput" } } } @@ -1327,7 +1329,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GetOrganizationUserOutput" } } } @@ -1375,7 +1377,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GetOrganizationUserOutput" } } } @@ -1411,7 +1413,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GetOrganizationUserOutput" } } } @@ -1422,6 +1424,197 @@ ] } }, + "/v2/organizations/{orgId}/memberships": { + "get": { + "operationId": "OrganizationsMembershipsController_getAllMemberships", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllOrgMemberships" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "post": { + "operationId": "OrganizationsMembershipsController_createMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipOutput" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsMembershipsController_getUserSchedule", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "delete": { + "operationId": "OrganizationsMembershipsController_deleteMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "patch": { + "operationId": "OrganizationsMembershipsController_updateMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, "/v2/schedules": { "post": { "operationId": "SchedulesController_2024_04_15_createSchedule", @@ -1534,16 +1727,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" - } - } - } - }, "responses": { "200": { "description": "", @@ -2424,6 +2607,54 @@ "type": "number", "nullable": true, "example": null + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" } }, "required": [ @@ -4421,57 +4652,6 @@ "data" ] }, - "CreateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - }, - "required": [ - "name", - "timeZone", - "isDefault" - ] - }, "CreateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -4520,52 +4700,6 @@ "data" ] }, - "UpdateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - } - }, "UpdateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -4605,87 +4739,142 @@ "status" ] }, - "GetOrganizationsUsersInput": { + "GetUserOutput": { "type": "object", "properties": { - "email": { - "type": "object", - "description": "The email address or an array of email addresses to filter by" - } - } - }, - "CreateOrganizationUserInput": { - "type": "object", - "properties": { - "locale": { - "type": "object", - "nullable": true, - "default": "en" - }, - "organizationRole": { - "type": "object", - "default": "MEMBER" - }, - "autoAccept": { - "type": "object", - "default": true - }, - "email": { - "type": "string" + "id": { + "type": "number", + "description": "The ID of the user", + "example": 1 }, "username": { - "type": "string" + "type": "string", + "nullable": true, + "description": "The username of the user", + "example": "john_doe" }, - "weekday": { - "type": "string" + "name": { + "type": "string", + "nullable": true, + "description": "The name of the user", + "example": "John Doe" }, - "brandColor": { - "type": "string" + "email": { + "type": "string", + "description": "The email of the user", + "example": "john@example.com" }, - "darkBrandColor": { - "type": "string" + "emailVerified": { + "format": "date-time", + "type": "string", + "nullable": true, + "description": "The date when the email was verified", + "example": "2022-01-01T00:00:00Z" }, - "hideBranding": { - "type": "boolean" + "bio": { + "type": "string", + "nullable": true, + "description": "The bio of the user", + "example": "I am a software developer" + }, + "avatarUrl": { + "type": "string", + "nullable": true, + "description": "The URL of the user's avatar", + "example": "https://example.com/avatar.jpg" }, "timeZone": { - "type": "string" - }, - "theme": { "type": "string", - "nullable": true + "description": "The time zone of the user", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "description": "The week start day of the user", + "example": "Monday" }, "appTheme": { "type": "string", - "nullable": true + "nullable": true, + "description": "The app theme of the user", + "example": "light" }, - "timeFormat": { - "type": "number" + "theme": { + "type": "string", + "nullable": true, + "description": "The theme of the user", + "example": "default" }, "defaultScheduleId": { "type": "number", - "minimum": 0 + "nullable": true, + "description": "The ID of the default schedule for the user", + "example": 1 }, - "avatarUrl": { - "type": "string" + "locale": { + "type": "string", + "nullable": true, + "description": "The locale of the user", + "example": "en-US" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "description": "The time format of the user", + "example": 12 + }, + "hideBranding": { + "type": "boolean", + "description": "Whether to hide branding for the user", + "example": false + }, + "brandColor": { + "type": "string", + "nullable": true, + "description": "The brand color of the user", + "example": "#ffffff" + }, + "darkBrandColor": { + "type": "string", + "nullable": true, + "description": "The dark brand color of the user", + "example": "#000000" + }, + "allowDynamicBooking": { + "type": "boolean", + "nullable": true, + "description": "Whether dynamic booking is allowed for the user", + "example": true + }, + "createdDate": { + "format": "date-time", + "type": "string", + "description": "The date when the user was created", + "example": "2022-01-01T00:00:00Z" + }, + "verified": { + "type": "boolean", + "nullable": true, + "description": "Whether the user is verified", + "example": true + }, + "invitedTo": { + "type": "number", + "nullable": true, + "description": "The ID of the user who invited this user", + "example": 1 } }, "required": [ - "locale", - "organizationRole", - "autoAccept", - "email" + "id", + "email", + "timeZone", + "weekStart", + "hideBranding", + "createdDate" ] }, - "UpdateOrganizationUserInput": { - "type": "object", - "properties": { - "email": { - "type": "string" - } - } - }, - "GetDefaultScheduleOutput_2024_06_11": { + "GetOrganizationUsersOutput": { "type": "object", "properties": { "status": { @@ -4697,7 +4886,308 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + "type": "array", + "items": { + "$ref": "#/components/schemas/GetUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "user123" + }, + "weekday": { + "type": "string", + "description": "Preferred weekday", + "example": "Monday" + }, + "brandColor": { + "type": "string", + "description": "Brand color in HEX format", + "example": "#FFFFFF" + }, + "darkBrandColor": { + "type": "string", + "description": "Dark brand color in HEX format", + "example": "#000000" + }, + "hideBranding": { + "type": "boolean", + "description": "Hide branding", + "example": false + }, + "timeZone": { + "type": "string", + "description": "Time zone", + "example": "America/New_York" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "Theme", + "example": "dark" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "Application theme", + "example": "light" + }, + "timeFormat": { + "type": "number", + "description": "Time format", + "example": 24 + }, + "defaultScheduleId": { + "type": "number", + "minimum": 0, + "description": "Default schedule ID", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "default": "en", + "description": "Locale", + "example": "en" + }, + "avatarUrl": { + "type": "string", + "description": "Avatar URL", + "example": "https://example.com/avatar.jpg" + }, + "organizationRole": { + "type": "object", + "default": "MEMBER" + }, + "autoAccept": { + "type": "object", + "default": true + } + }, + "required": [ + "email", + "organizationRole", + "autoAccept" + ] + }, + "GetOrganizationUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationUserInput": { + "type": "object", + "properties": {} + }, + "OrgMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "GetAllOrgMemberships": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "CreateOrgMembershipOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "UpdateOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" } }, "required": [ @@ -5002,66 +5492,6 @@ "data" ] }, - "UpdateScheduleInput_2024_04_15": { - "type": "object", - "properties": { - "timeZone": { - "type": "string" - }, - "name": { - "type": "string" - }, - "isDefault": { - "type": "boolean" - }, - "schedule": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" - }, - "dateOverrides": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" - } - }, - "required": [ - "timeZone", - "name", - "isDefault", - "schedule" - ] - }, "EventTypeModel_2024_04_15": { "type": "object", "properties": { diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index ae5a0d9cba..7332c9b438 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -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])