fix: api v2 get event-types non org users (#26896)

* fix: get event-types non org users

* fix: add excludeOrgUsers parameter to findByUsername for targeted filtering

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: create dedicated findByUsernameExcludingOrgUsers function

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* test: add e2e test for same username org vs non-org user event types

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* test: move e2e test for same username org vs non-org to 2024_06_14

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: add organizationId to orgUser in e2e test

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: reorder afterAll cleanup to delete users before team

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: add organizationId null check to findByUsernameExcludingOrgUsers

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* Revert "fix: add organizationId null check to findByUsernameExcludingOrgUsers"

This reverts commit 0d31e3d5d7c0eaa408939d697ea790521e520a3c.

* fixup! fix: add organizationId null check to findByUsernameExcludingOrgUsers

* fix: restore /public suffix in e2e test URL

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: use findByUsernameExcludingOrgUsers in getEventTypeByUsernameAndSlug when no org context

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fixup! fix: use findByUsernameExcludingOrgUsers in getEventTypeByUsernameAndSlug when no org context

* refactor: remove unused getEventTypesPublicByUsername from 2024_06_14 service

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: test user creation

* chore: add test for org event-types

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Morgan
2026-01-15 19:26:51 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent f5085af396
commit 77c8e34073
3 changed files with 185 additions and 21 deletions
@@ -3189,4 +3189,168 @@ describe("Event types Endpoints", () => {
await app.close();
});
});
describe("Same username - org vs non-org user", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const sharedUsername = `same-username-test-${randomString()}`;
const nonOrgUserEmail = `non-org-${sharedUsername}@api.com`;
const orgUserEmail = `org-${sharedUsername}@api.com`;
let nonOrgUser: User;
let orgUser: User;
let nonOrgUserEventType: EventType;
let orgUserEventType: EventType;
beforeAll(async () => {
const moduleRef = await withApiAuth(
nonOrgUserEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: `same-username-org-${randomString()}`,
slug: `same-username-org-slug-${randomString()}`,
});
oAuthClient = await oauthClientRepositoryFixture.create(
organization.id,
{
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
},
"secret"
);
nonOrgUser = await userRepositoryFixture.create({
email: nonOrgUserEmail,
name: `Non-Org User ${sharedUsername}`,
username: sharedUsername,
});
orgUser = await userRepositoryFixture.create({
email: orgUserEmail,
name: `Org User ${sharedUsername}`,
username: sharedUsername,
organization: {
connect: {
id: organization.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${orgUser.id}`,
username: sharedUsername,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: orgUser.id,
},
},
});
nonOrgUserEventType = await eventTypesRepositoryFixture.create(
{
title: "Non-Org User Event Type",
slug: `non-org-event-${randomString()}`,
length: 30,
hidden: false,
},
nonOrgUser.id
);
orgUserEventType = await eventTypesRepositoryFixture.create(
{
title: "Org User Event Type",
slug: `org-event-${randomString()}`,
length: 60,
hidden: false,
},
orgUser.id
);
await app.init();
});
it("should return only non-org user's event types when querying by shared username", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${sharedUsername}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].id).toEqual(nonOrgUserEventType.id);
expect(responseBody.data[0].title).toEqual("Non-Org User Event Type");
});
it("should return only org user's event types when querying by shared username with orgSlug", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${sharedUsername}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].id).toEqual(orgUserEventType.id);
expect(responseBody.data[0].title).toEqual("Org User Event Type");
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
try {
await eventTypesRepositoryFixture.delete(nonOrgUserEventType.id);
} catch (_e) {}
try {
await eventTypesRepositoryFixture.delete(orgUserEventType.id);
} catch (_e) {}
try {
await userRepositoryFixture.delete(nonOrgUser.id);
} catch (_e) {}
try {
await userRepositoryFixture.delete(orgUser.id);
} catch (_e) {}
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
});
@@ -16,12 +16,7 @@ import { UserWithProfile, UsersRepository } from "@/modules/users/users.reposito
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
createEventType,
updateEventType,
getEventTypesPublic,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
@@ -134,7 +129,10 @@ export class EventTypesService_2024_06_14 {
orgId?: number;
authUser?: AuthOptionalUser;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
const user =
params.orgSlug || params.orgId
? await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId)
: await this.usersRepository.findByUsernameExcludingOrgUsers(params.username);
if (!user) {
return null;
}
@@ -228,15 +226,6 @@ export class EventTypesService_2024_06_14 {
});
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
throw new NotFoundException(`User with username "${username}" not found`);
}
return await getEventTypesPublic(user.id);
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
const { username, eventSlug, usernames, orgSlug, orgId, sortCreatedAt } = queryParams;
if (username && eventSlug) {
@@ -1,11 +1,10 @@
import { CreationSource } from "@calcom/platform-libraries";
import type { Prisma, Profile, Team, User } from "@calcom/prisma/client";
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { Injectable, NotFoundException } from "@nestjs/common";
import { CreationSource } from "@calcom/platform-libraries";
import type { Profile, User, Team, Prisma } from "@calcom/prisma/client";
export type UserWithProfile = User & {
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
@@ -14,7 +13,10 @@ export type UserWithProfile = User & {
@Injectable()
export class UsersRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService
) {}
async create(
user: CreateManagedUserInput,
@@ -182,6 +184,15 @@ export class UsersRepository {
});
}
async findByUsernameExcludingOrgUsers(username: string) {
return this.dbRead.prisma.user.findFirst({
where: {
username,
profiles: { none: {} },
},
});
}
async findManagedUsersByOAuthClientId(oauthClientId: string, cursor: number, limit: number) {
return this.dbRead.prisma.user.findMany({
where: {