From ef498c6ed66e20040c3df31cc79556529005e4c0 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Mon, 25 Mar 2024 18:48:51 +0100 Subject: [PATCH] feat: distinguish managed platform users (#14207) * feat: add isPlatformManaged property to user * do not allow deleting non managed users * refactor: rename managed user create and update inputs * refactor response code * feat: add isPlatformManaged when creating managed user * fix TS --- apps/api/v2/src/ee/me/me.controller.e2e-spec.ts | 10 +++++----- apps/api/v2/src/ee/me/me.controller.ts | 4 ++-- .../oauth-client-users.controller.e2e-spec.ts | 10 +++++----- .../oauth-client-users.controller.ts | 16 +++++++++++----- .../services/oauth-clients-users.service.ts | 12 +++++++++--- ....ts => create-managed-platform-user.input.ts} | 2 +- ....ts => update-managed-platform-user.input.ts} | 2 +- .../api/v2/src/modules/users/users.repository.ts | 16 +++++++++++----- packages/lib/test/builder.ts | 1 + .../migration.sql | 2 ++ packages/prisma/schema.prisma | 1 + .../routers/viewer/teams/inviteMember/utils.ts | 3 +++ 12 files changed, 52 insertions(+), 27 deletions(-) rename apps/api/v2/src/modules/users/inputs/{create-user.input.ts => create-managed-platform-user.input.ts} (92%) rename apps/api/v2/src/modules/users/inputs/{update-user.input.ts => update-managed-platform-user.input.ts} (93%) create mode 100644 packages/prisma/migrations/20240325162556_add_user_is_platform_managed/migration.sql diff --git a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts b/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts index b5cf3c8e4b..d96bf283e9 100644 --- a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts @@ -6,7 +6,7 @@ import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions. import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; +import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input"; import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; import { NestExpressApplication } from "@nestjs/platform-express"; @@ -81,7 +81,7 @@ describe("Me Endpoints", () => { }); it("should update user associated with access token", async () => { - const body: UpdateUserInput = { timeZone: "Europe/Rome" }; + const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" }; return request(app.getHttpServer()) .patch("/api/v2/me") @@ -106,19 +106,19 @@ describe("Me Endpoints", () => { }); it("should not update user associated with access token given invalid timezone", async () => { - const bodyWithIncorrectTimeZone: UpdateUserInput = { timeZone: "Narnia/Woods" }; + const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" }; return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeZone).expect(400); }); it("should not update user associated with access token given invalid time format", async () => { - const bodyWithIncorrectTimeFormat: UpdateUserInput = { timeFormat: 100 }; + const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 }; return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeFormat).expect(400); }); it("should not update user associated with access token given invalid week start", async () => { - const bodyWithIncorrectWeekStart: UpdateUserInput = { weekStart: "waba luba dub dub" }; + const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" }; return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectWeekStart).expect(400); }); diff --git a/apps/api/v2/src/ee/me/me.controller.ts b/apps/api/v2/src/ee/me/me.controller.ts index 81cda17469..0cb9216771 100644 --- a/apps/api/v2/src/ee/me/me.controller.ts +++ b/apps/api/v2/src/ee/me/me.controller.ts @@ -3,7 +3,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; +import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input"; import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common"; @@ -37,7 +37,7 @@ export class MeController { @Permissions([PROFILE_WRITE]) async updateMe( @GetUser() user: UserWithProfile, - @Body() bodySchedule: UpdateUserInput + @Body() bodySchedule: UpdateManagedPlatformUserInput ): Promise> { const updatedUser = await this.usersRepository.update(user.id, bodySchedule); if (bodySchedule.timeZone && user.defaultScheduleId) { diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 7828ac5d30..fd9fce0f15 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -7,8 +7,8 @@ import { CreateUserResponse, UserReturned, } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; +import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input"; +import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input"; import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; import { NestExpressApplication } from "@nestjs/platform-express"; @@ -117,7 +117,7 @@ describe("OAuth Client Users Endpoints", () => { }); it(`should fail /POST with incorrect timeZone`, async () => { - const requestBody: CreateUserInput = { + const requestBody: CreateManagedPlatformUserInput = { email: "oauth-client-user@gmail.com", timeZone: "incorrect-time-zone", }; @@ -130,7 +130,7 @@ describe("OAuth Client Users Endpoints", () => { }); it(`/POST`, async () => { - const requestBody: CreateUserInput = { + const requestBody: CreateManagedPlatformUserInput = { email: "oauth-client-user@gmail.com", }; @@ -195,7 +195,7 @@ describe("OAuth Client Users Endpoints", () => { it(`/PUT/:id`, async () => { const userUpdatedEmail = "pineapple-pizza@gmail.com"; - const body: UpdateUserInput = { email: userUpdatedEmail }; + const body: UpdateManagedPlatformUserInput = { email: userUpdatedEmail }; const response = await request(app.getHttpServer()) .patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index f53ee7f1b8..26a92ab3cd 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -3,8 +3,8 @@ import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-toke import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; +import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input"; +import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input"; import { UsersRepository } from "@/modules/users/users.repository"; import { Body, @@ -43,7 +43,7 @@ export class OAuthClientUsersController { @UseGuards(OAuthClientCredentialsGuard) async createUser( @Param("clientId") oAuthClientId: string, - @Body() body: CreateUserInput + @Body() body: CreateManagedPlatformUserInput ): Promise> { this.logger.log( `Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}` @@ -55,9 +55,11 @@ export class OAuthClientUsersController { } const client = await this.oauthRepository.getOAuthClient(oAuthClientId); + const isPlatformManaged = true; const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser( oAuthClientId, body, + isPlatformManaged, client?.organizationId ); @@ -113,7 +115,7 @@ export class OAuthClientUsersController { @Param("clientId") _: string, @GetUser("id") accessTokenUserId: number, @Param("userId") userId: number, - @Body() body: UpdateUserInput + @Body() body: UpdateManagedPlatformUserInput ): Promise> { if (accessTokenUserId !== userId) { throw new BadRequestException("userId parameter does not match access token"); @@ -152,7 +154,11 @@ export class OAuthClientUsersController { const existingUser = await this.userRepository.findById(userId); if (!existingUser) { - throw new NotFoundException(`User with ${userId} does not exist`); + throw new NotFoundException(`User with ID=${userId} does not exist`); + } + + if (!existingUser.isPlatformManaged) { + throw new BadRequestException(`Can't delete non managed user with ID=${userId}`); } const user = await this.userRepository.delete(userId); diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 7fc51ebf05..88a140d862 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -1,6 +1,6 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; +import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input"; import { UsersRepository } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; import { User } from "@prisma/client"; @@ -16,11 +16,16 @@ export class OAuthClientUsersService { private readonly eventTypesService: EventTypesService ) {} - async createOauthClientUser(oAuthClientId: string, body: CreateUserInput, organizationId?: number) { + async createOauthClientUser( + oAuthClientId: string, + body: CreateManagedPlatformUserInput, + isPlatformManaged: boolean, + organizationId?: number + ) { let user: User; if (!organizationId) { const username = generateShortHash(body.email, oAuthClientId); - user = await this.userRepository.create(body, username, oAuthClientId); + user = await this.userRepository.create(body, username, oAuthClientId, isPlatformManaged); } else { const [_, emailDomain] = body.email.split("@"); user = ( @@ -41,6 +46,7 @@ export class OAuthClientUsersService { autoAccept: true, }, }, + isPlatformManaged, }) )[0]; await this.userRepository.addToOAuthClient(user.id, oAuthClientId); diff --git a/apps/api/v2/src/modules/users/inputs/create-user.input.ts b/apps/api/v2/src/modules/users/inputs/create-managed-platform-user.input.ts similarity index 92% rename from apps/api/v2/src/modules/users/inputs/create-user.input.ts rename to apps/api/v2/src/modules/users/inputs/create-managed-platform-user.input.ts index 0d13750aa4..c8d0c577af 100644 --- a/apps/api/v2/src/modules/users/inputs/create-user.input.ts +++ b/apps/api/v2/src/modules/users/inputs/create-managed-platform-user.input.ts @@ -2,7 +2,7 @@ import { IsTimeFormat } from "@/modules/users/inputs/validators/is-time-format"; import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start"; import { IsNumber, IsOptional, IsTimeZone, IsString, Validate } from "class-validator"; -export class CreateUserInput { +export class CreateManagedPlatformUserInput { @IsString() email!: string; diff --git a/apps/api/v2/src/modules/users/inputs/update-user.input.ts b/apps/api/v2/src/modules/users/inputs/update-managed-platform-user.input.ts similarity index 93% rename from apps/api/v2/src/modules/users/inputs/update-user.input.ts rename to apps/api/v2/src/modules/users/inputs/update-managed-platform-user.input.ts index db0caba290..c01cfdbc76 100644 --- a/apps/api/v2/src/modules/users/inputs/update-user.input.ts +++ b/apps/api/v2/src/modules/users/inputs/update-managed-platform-user.input.ts @@ -2,7 +2,7 @@ import { IsTimeFormat } from "@/modules/users/inputs/validators/is-time-format"; import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start"; import { IsNumber, IsOptional, IsString, IsTimeZone, Validate } from "class-validator"; -export class UpdateUserInput { +export class UpdateManagedPlatformUserInput { @IsString() @IsOptional() email?: string; diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index 1557dfe00a..7ee095fbc6 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -1,7 +1,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; +import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input"; +import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input"; import { Injectable } from "@nestjs/common"; import type { Profile, User } from "@prisma/client"; @@ -13,7 +13,12 @@ export type UserWithProfile = User & { export class UsersRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - async create(user: CreateUserInput, username: string, oAuthClientId: string) { + async create( + user: CreateManagedPlatformUserInput, + username: string, + oAuthClientId: string, + isPlatformManaged: boolean + ) { this.formatInput(user); return this.dbRead.prisma.user.create({ @@ -23,6 +28,7 @@ export class UsersRepository { platformOAuthClients: { connect: { id: oAuthClientId }, }, + isPlatformManaged, }, }); } @@ -77,7 +83,7 @@ export class UsersRepository { }); } - async update(userId: number, updateData: UpdateUserInput) { + async update(userId: number, updateData: UpdateManagedPlatformUserInput) { this.formatInput(updateData); return this.dbWrite.prisma.user.update({ @@ -92,7 +98,7 @@ export class UsersRepository { }); } - formatInput(userInput: CreateUserInput | UpdateUserInput) { + formatInput(userInput: CreateManagedPlatformUserInput | UpdateManagedPlatformUserInput) { if (userInput.weekStart) { userInput.weekStart = capitalize(userInput.weekStart); } diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 575aa39eed..ddf6a45a29 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -249,6 +249,7 @@ export const buildUser = >( receiveMonthlyDigestEmail: null, movedToProfileId: null, priority: user?.priority ?? null, + isPlatformManaged: false, ...user, }; }; diff --git a/packages/prisma/migrations/20240325162556_add_user_is_platform_managed/migration.sql b/packages/prisma/migrations/20240325162556_add_user_is_platform_managed/migration.sql new file mode 100644 index 0000000000..f580d92a07 --- /dev/null +++ b/packages/prisma/migrations/20240325162556_add_user_is_platform_managed/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "isPlatformManaged" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index f1dea05277..104d599f9f 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -300,6 +300,7 @@ model User { movedToProfileId Int? movedToProfile Profile? @relation("moved_to_profile", fields: [movedToProfileId], references: [id], onDelete: SetNull) secondaryEmails SecondaryEmail[] + isPlatformManaged Boolean @default(false) @@unique([email]) @@unique([email, username]) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 19f9fd717f..f344a48413 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -224,12 +224,14 @@ export async function createNewUsersConnectToOrgIfExists({ parentId, autoAcceptEmailDomain, connectionInfoMap, + isPlatformManaged, }: { usernamesOrEmails: string[]; input: InviteMemberOptions["input"]; parentId?: number | null; autoAcceptEmailDomain?: string; connectionInfoMap: Record>; + isPlatformManaged?: boolean; }) { // fail if we have invalid emails usernamesOrEmails.forEach((usernameOrEmail) => checkInputEmailIsValid(usernameOrEmail)); @@ -260,6 +262,7 @@ export async function createNewUsersConnectToOrgIfExists({ email: usernameOrEmail, verified: true, invitedTo: input.teamId, + isPlatformManaged: !!isPlatformManaged, organizationId: orgId || null, // If the user is invited to a child team, they are automatically added to the parent org ...(orgId ? {