Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: distinguish managed platform users #14207

Merged
merged 6 commits into from Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/api/v2/src/ee/me/me.controller.e2e-spec.ts
Expand Up @@ -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";
Expand Down Expand Up @@ -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")
Expand All @@ -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);
});
Expand Down
4 changes: 2 additions & 2 deletions apps/api/v2/src/ee/me/me.controller.ts
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
};
Expand All @@ -130,7 +130,7 @@ describe("OAuth Client Users Endpoints", () => {
});

it(`/POST`, async () => {
const requestBody: CreateUserInput = {
const requestBody: CreateManagedPlatformUserInput = {
email: "oauth-client-user@gmail.com",
};

Expand Down Expand Up @@ -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}`)
Expand Down
Expand Up @@ -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,
Expand Down Expand Up @@ -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}`
Expand All @@ -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
);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
@@ -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";
Expand All @@ -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 = (
Expand All @@ -41,6 +46,7 @@ export class OAuthClientUsersService {
autoAccept: true,
},
},
isPlatformManaged,
})
)[0];
await this.userRepository.addToOAuthClient(user.id, oAuthClientId);
Expand Down
Expand Up @@ -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;

Expand Down
Expand Up @@ -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;
Expand Down
16 changes: 11 additions & 5 deletions 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";

Expand All @@ -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({
Expand All @@ -23,6 +28,7 @@ export class UsersRepository {
platformOAuthClients: {
connect: { id: oAuthClientId },
},
isPlatformManaged,
},
});
}
Expand Down Expand Up @@ -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({
Expand All @@ -92,7 +98,7 @@ export class UsersRepository {
});
}

formatInput(userInput: CreateUserInput | UpdateUserInput) {
formatInput(userInput: CreateManagedPlatformUserInput | UpdateManagedPlatformUserInput) {
if (userInput.weekStart) {
userInput.weekStart = capitalize(userInput.weekStart);
}
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/builder.ts
Expand Up @@ -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 change: 1 addition & 0 deletions packages/prisma/schema.prisma
Expand Up @@ -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])
Expand Down
Expand Up @@ -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));
Expand Down Expand Up @@ -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
? {
Expand Down