diff --git a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.controller.ts b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.controller.ts index bf855418d9..ebabb65aa9 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.controller.ts @@ -12,10 +12,12 @@ import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; import { CreateOrganizationUserInput } from "@/modules/organizations/inputs/create-organization-user.input"; import { GetOrganizationsUsersInput } from "@/modules/organizations/inputs/get-organization-users.input"; import { UpdateOrganizationUserInput } from "@/modules/organizations/inputs/update-organization-user.input"; -import { GetOrganizationUsersOutput } from "@/modules/organizations/outputs/get-organization-users.output"; +import { + GetOrganizationUsersResponseDTO, + GetOrgUsersWithProfileOutput, +} from "@/modules/organizations/outputs/get-organization-users.output"; import { GetOrganizationUserOutput } from "@/modules/organizations/outputs/get-organization-users.output"; import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service"; -import { GetUserOutput } from "@/modules/users/outputs/get-users.output"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, @@ -55,7 +57,7 @@ export class OrganizationsUsersController { async getOrganizationsUsers( @Param("orgId", ParseIntPipe) orgId: number, @Query() query: GetOrganizationsUsersInput - ): Promise { + ): Promise { const users = await this.organizationsUsersService.getUsers( orgId, query.emails, @@ -65,7 +67,13 @@ export class OrganizationsUsersController { return { status: SUCCESS_STATUS, - data: users.map((user) => plainToInstance(GetUserOutput, user, { strategy: "excludeAll" })), + data: users.map((user) => + plainToInstance( + GetOrgUsersWithProfileOutput, + { ...user, profile: user?.profiles?.[0] ?? {} }, + { strategy: "excludeAll" } + ) + ), }; } @@ -86,7 +94,11 @@ export class OrganizationsUsersController { ); return { status: SUCCESS_STATUS, - data: plainToInstance(GetUserOutput, user, { strategy: "excludeAll" }), + data: plainToInstance( + GetOrgUsersWithProfileOutput, + { ...user, profile: user?.profiles?.[0] ?? {} }, + { strategy: "excludeAll" } + ), }; } @@ -104,7 +116,11 @@ export class OrganizationsUsersController { const user = await this.organizationsUsersService.updateUser(orgId, userId, input); return { status: SUCCESS_STATUS, - data: plainToInstance(GetUserOutput, user, { strategy: "excludeAll" }), + data: plainToInstance( + GetOrgUsersWithProfileOutput, + { ...user, profile: user?.profiles?.[0] ?? {} }, + { strategy: "excludeAll" } + ), }; } @@ -120,7 +136,11 @@ export class OrganizationsUsersController { const user = await this.organizationsUsersService.deleteUser(orgId, userId); return { status: SUCCESS_STATUS, - data: plainToInstance(GetUserOutput, user, { strategy: "excludeAll" }), + data: plainToInstance( + GetOrgUsersWithProfileOutput, + { ...user, profile: user?.profiles?.[0] ?? {} }, + { strategy: "excludeAll" } + ), }; } } diff --git a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts index d1ca934384..2257d5813d 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts @@ -1,6 +1,7 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { EmailService } from "@/modules/email/email.service"; +import { GetOrgUsersWithProfileOutput } from "@/modules/organizations/outputs/get-organization-users.output"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; @@ -233,10 +234,18 @@ describe("Organizations Users Endpoints", () => { it("should get all org users", async () => { const { body } = await request(app.getHttpServer()).get(`/v2/organizations/${org.id}/users`); - const userData = body.data; + const userData = body.data as GetOrgUsersWithProfileOutput[]; expect(body.status).toBe(SUCCESS_STATUS); expect(userData.length).toBe(4); + console.log( + "profiles", + { userData }, + userData.map((u) => u.profile) + ); + expect(userData.find((u) => u.profile.username === "member1@org.com")).toBeDefined(); + expect(userData.find((u) => u.profile.username === "member2@org.com")).toBeDefined(); + expect(userData.find((u) => u.profile.username === "member3@org.com")).toBeDefined(); expect(userData.filter((user: { email: string }) => user.email === nonMemberEmail).length).toBe(0); }); @@ -250,12 +259,13 @@ describe("Organizations Users Endpoints", () => { .set("Content-Type", "application/json") .set("Accept", "application/json"); - const userData = body.data; + const userData = body.data as GetOrgUsersWithProfileOutput[]; expect(body.status).toBe(SUCCESS_STATUS); expect(userData.length).toBe(1); - expect(userData.filter((user: { email: string }) => user.email === userEmail).length).toBe(1); + expect(userData.filter((user) => user.email === userEmail).length).toBe(1); + expect(userData.find((u) => u.profile.username === user.username)).toBeDefined(); }); it("should get users within the specified emails array", async () => { diff --git a/apps/api/v2/src/modules/organizations/outputs/get-organization-users.output.ts b/apps/api/v2/src/modules/organizations/outputs/get-organization-users.output.ts index 0b0b53a3f9..5bb90c95d4 100644 --- a/apps/api/v2/src/modules/organizations/outputs/get-organization-users.output.ts +++ b/apps/api/v2/src/modules/organizations/outputs/get-organization-users.output.ts @@ -1,20 +1,64 @@ import { GetUserOutput } from "@/modules/users/outputs/get-users.output"; import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum } from "class-validator"; +import { Expose, Type } from "class-transformer"; +import { IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator"; import { ERROR_STATUS } from "@calcom/platform-constants"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; -export class GetOrganizationUsersOutput { +export class ProfileOutput { + @IsInt() + @Expose() + @ApiProperty({ type: Number, required: true, description: "The ID of the profile of user", example: 1 }) + id!: number; + + @IsInt() + @Expose() + @ApiProperty({ + type: Number, + required: true, + description: "The ID of the organization of user", + example: 1, + }) + organizationId!: number; + + @IsInt() + @Expose() + @ApiProperty({ type: Number, required: true, description: "The IDof the user", example: 1 }) + userId!: number; + + @IsString() + @Expose() + @ApiProperty({ + type: String, + nullable: true, + required: false, + description: "The username of the user within the organization context", + example: "john_doe", + }) + username!: string; +} +export class GetOrgUsersWithProfileOutput extends GetUserOutput { + @ApiProperty({ + description: "organization user profile, contains user data within the organizaton context", + }) + @Expose() + @ValidateNested() + @IsArray() + @Type(() => ProfileOutput) + profile!: ProfileOutput; +} + +export class GetOrganizationUsersResponseDTO { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - data!: GetUserOutput[]; + data!: GetOrgUsersWithProfileOutput[]; } export class GetOrganizationUserOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - data!: GetUserOutput; + data!: GetOrgUsersWithProfileOutput; } diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-users.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-users.repository.ts index 5612972e5c..b126a001e4 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-users.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-users.repository.ts @@ -24,6 +24,13 @@ export class OrganizationsUsersRepository { ...this.filterOnOrgMembership(orgId), ...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}), }, + include: { + profiles: { + where: { + organizationId: orgId, + }, + }, + }, skip, take, }); @@ -35,6 +42,13 @@ export class OrganizationsUsersRepository { username, ...this.filterOnOrgMembership(orgId), }, + include: { + profiles: { + where: { + organizationId: orgId, + }, + }, + }, }); } @@ -44,6 +58,13 @@ export class OrganizationsUsersRepository { email, ...this.filterOnOrgMembership(orgId), }, + include: { + profiles: { + where: { + organizationId: orgId, + }, + }, + }, }); } @@ -62,6 +83,13 @@ export class OrganizationsUsersRepository { organizationId: orgId, }, data: updateUserBody, + include: { + profiles: { + where: { + organizationId: orgId, + }, + }, + }, }); } @@ -71,6 +99,13 @@ export class OrganizationsUsersRepository { id: userId, organizationId: orgId, }, + include: { + profiles: { + where: { + organizationId: orgId, + }, + }, + }, }); } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 5edde71e5e..5e4d600c5d 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2548,7 +2548,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOrganizationUsersOutput" + "$ref": "#/components/schemas/GetOrganizationUsersResponseDTO" } } } @@ -4176,15 +4176,6 @@ "type": "string" } }, - { - "name": "eventTypeId", - "required": false, - "in": "query", - "description": "Id of the event-type, if this is provided no other parameter is required", - "schema": { - "type": "number" - } - }, { "name": "usernames", "required": false, @@ -4194,15 +4185,6 @@ "type": "string" } }, - { - "name": "userId", - "required": false, - "in": "query", - "description": "ID of the user you want the get the event-types of, username is not needed when using this parameter", - "schema": { - "type": "number" - } - }, { "name": "orgSlug", "required": false, @@ -9784,7 +9766,38 @@ "status" ] }, - "GetUserOutput": { + "ProfileOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the profile of user", + "example": 1 + }, + "organizationId": { + "type": "number", + "description": "The ID of the organization of user", + "example": 1 + }, + "userId": { + "type": "number", + "description": "The IDof the user", + "example": 1 + }, + "username": { + "type": "string", + "nullable": true, + "description": "The username of the user within the organization context", + "example": "john_doe" + } + }, + "required": [ + "id", + "organizationId", + "userId" + ] + }, + "GetOrgUsersWithProfileOutput": { "type": "object", "properties": { "id": { @@ -9908,6 +9921,14 @@ "nullable": true, "description": "The ID of the user who invited this user", "example": 1 + }, + "profile": { + "description": "organization user profile, contains user data within the organizaton context", + "allOf": [ + { + "$ref": "#/components/schemas/ProfileOutput" + } + ] } }, "required": [ @@ -9916,10 +9937,11 @@ "timeZone", "weekStart", "hideBranding", - "createdDate" + "createdDate", + "profile" ] }, - "GetOrganizationUsersOutput": { + "GetOrganizationUsersResponseDTO": { "type": "object", "properties": { "status": { @@ -9933,7 +9955,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/GetOrgUsersWithProfileOutput" } } }, @@ -10042,7 +10064,7 @@ ] }, "data": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/GetOrgUsersWithProfileOutput" } }, "required": [ diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index f1c6d1548b..4af87be068 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -2421,7 +2421,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOrganizationUsersOutput" + "$ref": "#/components/schemas/GetOrganizationUsersResponseDTO" } } } @@ -3960,15 +3960,6 @@ "type": "string" } }, - { - "name": "eventTypeId", - "required": false, - "in": "query", - "description": "Id of the event-type, if this is provided no other parameter is required", - "schema": { - "type": "number" - } - }, { "name": "usernames", "required": false, @@ -3978,15 +3969,6 @@ "type": "string" } }, - { - "name": "userId", - "required": false, - "in": "query", - "description": "ID of the user you want the get the event-types of, username is not needed when using this parameter", - "schema": { - "type": "number" - } - }, { "name": "orgSlug", "required": false, @@ -8942,7 +8924,34 @@ }, "required": ["status"] }, - "GetUserOutput": { + "ProfileOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the profile of user", + "example": 1 + }, + "organizationId": { + "type": "number", + "description": "The ID of the organization of user", + "example": 1 + }, + "userId": { + "type": "number", + "description": "The IDof the user", + "example": 1 + }, + "username": { + "type": "string", + "nullable": true, + "description": "The username of the user within the organization context", + "example": "john_doe" + } + }, + "required": ["id", "organizationId", "userId"] + }, + "GetOrgUsersWithProfileOutput": { "type": "object", "properties": { "id": { @@ -9066,11 +9075,19 @@ "nullable": true, "description": "The ID of the user who invited this user", "example": 1 + }, + "profile": { + "description": "organization user profile, contains user data within the organizaton context", + "allOf": [ + { + "$ref": "#/components/schemas/ProfileOutput" + } + ] } }, - "required": ["id", "email", "timeZone", "weekStart", "hideBranding", "createdDate"] + "required": ["id", "email", "timeZone", "weekStart", "hideBranding", "createdDate", "profile"] }, - "GetOrganizationUsersOutput": { + "GetOrganizationUsersResponseDTO": { "type": "object", "properties": { "status": { @@ -9081,7 +9098,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/GetOrgUsersWithProfileOutput" } } }, @@ -9180,7 +9197,7 @@ "enum": ["success", "error"] }, "data": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/GetOrgUsersWithProfileOutput" } }, "required": ["status", "data"]