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
This commit is contained in:
Lauris Skraucis
2024-03-25 18:48:51 +01:00
committed by GitHub
parent bec1453148
commit ef498c6ed6
12 changed files with 52 additions and 27 deletions
@@ -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);
});
+2 -2
View File
@@ -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<ApiResponse<UserResponse>> {
const updatedUser = await this.usersRepository.update(user.id, bodySchedule);
if (bodySchedule.timeZone && user.defaultScheduleId) {
@@ -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}`)
@@ -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<ApiResponse<CreateUserResponse>> {
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<ApiResponse<UserReturned>> {
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);
@@ -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);
@@ -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;
@@ -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;
@@ -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);
}
+1
View File
@@ -249,6 +249,7 @@ export const buildUser = <T extends Partial<UserPayload>>(
receiveMonthlyDigestEmail: null,
movedToProfileId: null,
priority: user?.priority ?? null,
isPlatformManaged: false,
...user,
};
};
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "isPlatformManaged" BOOLEAN NOT NULL DEFAULT false;
+1
View File
@@ -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])
@@ -224,12 +224,14 @@ export async function createNewUsersConnectToOrgIfExists({
parentId,
autoAcceptEmailDomain,
connectionInfoMap,
isPlatformManaged,
}: {
usernamesOrEmails: string[];
input: InviteMemberOptions["input"];
parentId?: number | null;
autoAcceptEmailDomain?: string;
connectionInfoMap: Record<string, ReturnType<typeof getOrgConnectionInfo>>;
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
? {