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 82bc9cf486..d69ae85390 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 @@ -9,8 +9,10 @@ import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; -import { User } from "@prisma/client"; +import { User, Team } from "@prisma/client"; import * as request from "supertest"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withApiAuth } from "test/utils/withApiAuth"; @@ -25,9 +27,11 @@ describe("Me Endpoints", () => { let userRepositoryFixture: UserRepositoryFixture; let schedulesRepositoryFixture: SchedulesRepositoryFixture; - + let profilesRepositoryFixture: ProfileRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; const userEmail = "me-controller-e2e@api.com"; let user: User; + let org: Team; beforeAll(async () => { const moduleRef = await withApiAuth( @@ -43,6 +47,9 @@ describe("Me Endpoints", () => { .compile(); userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef); user = await userRepositoryFixture.create({ @@ -50,6 +57,20 @@ describe("Me Endpoints", () => { username: userEmail, }); + org = await organizationsRepositoryFixture.create({ + name: "Test org team", + isOrganization: true, + isPlatform: true, + }); + + await profilesRepositoryFixture.create({ + uid: "asd-asd", + username: userEmail, + user: { connect: { id: user.id } }, + organization: { connect: { id: org.id } }, + movedFromUser: { connect: { id: user.id } }, + }); + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -75,6 +96,8 @@ describe("Me Endpoints", () => { expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId); expect(responseBody.data.weekStart).toEqual(user.weekStart); expect(responseBody.data.timeZone).toEqual(user.timeZone); + expect(responseBody.data.organization?.isPlatform).toEqual(true); + expect(responseBody.data.organization?.id).toEqual(org.id); }); }); @@ -138,6 +161,7 @@ describe("Me Endpoints", () => { afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); + await organizationsRepositoryFixture.delete(org.id); await app.close(); }); }); diff --git a/apps/api/v2/src/ee/me/me.controller.ts b/apps/api/v2/src/ee/me/me.controller.ts index c22a9a606e..b03583d6e7 100644 --- a/apps/api/v2/src/ee/me/me.controller.ts +++ b/apps/api/v2/src/ee/me/me.controller.ts @@ -29,8 +29,19 @@ export class MeController { @Get("/") @Permissions([PROFILE_READ]) async getMe(@GetUser() user: UserWithProfile): Promise { - const me = userSchemaResponse.parse(user); - + const organization = user?.movedToProfile?.organization; + const me = userSchemaResponse.parse( + organization + ? { + ...user, + organizationId: organization.id, + organization: { + id: organization.id, + isPlatform: organization.isPlatform, + }, + } + : user + ); return { status: SUCCESS_STATUS, data: me, diff --git a/apps/api/v2/src/ee/me/outputs/me.output.ts b/apps/api/v2/src/ee/me/outputs/me.output.ts index 597b2738e9..64c3fb6483 100644 --- a/apps/api/v2/src/ee/me/outputs/me.output.ts +++ b/apps/api/v2/src/ee/me/outputs/me.output.ts @@ -1,5 +1,11 @@ -import { IsInt, IsEmail, IsOptional, IsString } from "class-validator"; +import { Type } from "class-transformer"; +import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator"; +export class MeOrgOutput { + isPlatform!: boolean; + + id!: number; +} export class MeOutput { @IsInt() id!: number; @@ -25,4 +31,9 @@ export class MeOutput { @IsInt() organizationId!: number | null; + + @IsOptional() + @ValidateNested() + @Type(() => MeOrgOutput) + organization?: MeOrgOutput; } diff --git a/apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts b/apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts index ee213059fc..2a427d08df 100644 --- a/apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts @@ -4,6 +4,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Reflector } from "@nestjs/core"; +import { getToken } from "next-auth/jwt"; import { hasPermissions } from "@calcom/platform-utils"; @@ -24,6 +25,12 @@ export class PermissionsGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const authString = request.get("Authorization")?.replace("Bearer ", ""); + const nextAuthSecret = this.config.get("next.authSecret", { infer: true }); + const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret }); + + if (nextAuthToken) { + return true; + } if (!authString) { return false; diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts index 17df03b9ad..921e8cef83 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts @@ -11,6 +11,7 @@ import { Injectable, InternalServerErrorException, UnauthorizedException } from import { ConfigService } from "@nestjs/config"; import { PassportStrategy } from "@nestjs/passport"; import type { Request } from "express"; +import { getToken } from "next-auth/jwt"; import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; @@ -45,6 +46,13 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") return await this.authenticateBearerToken(bearerToken, requestOrigin); } + const nextAuthSecret = this.config.get("next.authSecret", { infer: true }); + const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret }); + + if (nextAuthToken) { + return await this.authenticateNextAuth(nextAuthToken); + } + throw new UnauthorizedException( "No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers" ); @@ -58,6 +66,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") } } + async authenticateNextAuth(token: { email?: string | null }) { + const user = await this.nextAuthStrategy(token); + return this.success(user); + } + async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) { const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret); return this.success(user); @@ -163,4 +176,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId); return user; } + + async nextAuthStrategy(token: { email?: string | null }) { + if (!token.email) { + throw new UnauthorizedException("Email not found in the authentication token."); + } + + const user = await this.userRepository.findByEmailWithProfile(token.email); + if (!user) { + throw new UnauthorizedException("User associated with the authentication token email not found."); + } + + return user; + } } diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index a4fa88daa4..0303a5117f 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -3,10 +3,10 @@ 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 } from "@nestjs/common"; -import type { Profile, User } from "@prisma/client"; +import type { Profile, User, Team } from "@prisma/client"; export type UserWithProfile = User & { - movedToProfile?: Profile | null; + movedToProfile?: (Profile & { organization: Pick }) | null; }; @Injectable() @@ -67,12 +67,15 @@ export class UsersRepository { } async findByIdWithProfile(userId: number): Promise { + console.log("findByIdWithProfile"); return this.dbRead.prisma.user.findUnique({ where: { id: userId, }, include: { - movedToProfile: true, + movedToProfile: { + include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } }, + }, }, }); } @@ -126,7 +129,9 @@ export class UsersRepository { email, }, include: { - movedToProfile: true, + movedToProfile: { + include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } }, + }, }, }); } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index a293fb3d6c..3ee2180616 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -1219,16 +1219,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" - } - } - } - }, "responses": { "201": { "description": "", @@ -1331,16 +1321,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" - } - } - } - }, "responses": { "200": { "description": "", @@ -2380,16 +2360,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" - } - } - } - }, "responses": { "200": { "description": "", @@ -6071,57 +6041,6 @@ "data" ] }, - "CreateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - }, - "required": [ - "name", - "timeZone", - "isDefault" - ] - }, "CreateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -6170,52 +6089,6 @@ "data" ] }, - "UpdateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - } - }, "UpdateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -7310,26 +7183,6 @@ "userId" ] }, - "GetDefaultScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - }, - "required": [ - "status", - "data" - ] - }, "CreateAvailabilityInput_2024_04_15": { "type": "object", "properties": { @@ -7627,66 +7480,6 @@ "data" ] }, - "UpdateScheduleInput_2024_04_15": { - "type": "object", - "properties": { - "timeZone": { - "type": "string" - }, - "name": { - "type": "string" - }, - "isDefault": { - "type": "boolean" - }, - "schedule": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" - }, - "dateOverrides": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" - } - }, - "required": [ - "timeZone", - "name", - "isDefault", - "schedule" - ] - }, "EventTypeModel_2024_04_15": { "type": "object", "properties": { @@ -7932,6 +7725,21 @@ "status" ] }, + "MeOrgOutput": { + "type": "object", + "properties": { + "isPlatform": { + "type": "boolean" + }, + "id": { + "type": "number" + } + }, + "required": [ + "isPlatform", + "id" + ] + }, "MeOutput": { "type": "object", "properties": { @@ -7960,6 +7768,9 @@ "organizationId": { "type": "number", "nullable": true + }, + "organization": { + "$ref": "#/components/schemas/MeOrgOutput" } }, "required": [ diff --git a/apps/web/components/settings/platform/hooks/useGetUserAttributes.ts b/apps/web/components/settings/platform/hooks/useGetUserAttributes.ts index 8e5d0e5c67..31f0dc1137 100644 --- a/apps/web/components/settings/platform/hooks/useGetUserAttributes.ts +++ b/apps/web/components/settings/platform/hooks/useGetUserAttributes.ts @@ -1,15 +1,23 @@ -import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { useCheckTeamBilling } from "@calcom/web/lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient"; -export const useGetUserAttributes = () => { - const { data: user, isLoading: isUserLoading } = useMeQuery(); - const { data: userBillingData, isFetching: isUserBillingDataLoading } = useCheckTeamBilling( - user?.organizationId, - user?.organization.isPlatform - ); - const isPlatformUser = user?.organization.isPlatform; - const isPaidUser = userBillingData?.valid; - const userOrgId = user?.organizationId; +import { usePlatformMe } from "./usePlatformMe"; - return { isUserLoading, isUserBillingDataLoading, isPlatformUser, isPaidUser, userBillingData, userOrgId }; +export const useGetUserAttributes = () => { + const { data: platformUser, isLoading: isPlatformUserLoading } = usePlatformMe(); + const { data: userBillingData, isFetching: isUserBillingDataLoading } = useCheckTeamBilling( + platformUser?.organizationId, + platformUser?.organization.isPlatform + ); + const isPlatformUser = platformUser?.organization.isPlatform; + const isPaidUser = userBillingData?.valid; + const userOrgId = platformUser?.organizationId; + + return { + isUserLoading: isPlatformUserLoading, + isUserBillingDataLoading, + isPlatformUser, + isPaidUser, + userBillingData, + userOrgId, + }; }; diff --git a/apps/web/components/settings/platform/hooks/usePlatformMe.ts b/apps/web/components/settings/platform/hooks/usePlatformMe.ts new file mode 100644 index 0000000000..f08361350c --- /dev/null +++ b/apps/web/components/settings/platform/hooks/usePlatformMe.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; + +export const usePlatformMe = () => { + const QUERY_KEY = "get-platform-me"; + const platformMeQuery = useQuery({ + queryKey: [QUERY_KEY], + queryFn: async () => { + const response = await fetch(`/api/v2/me`, { + method: "get", + headers: { "Content-type": "application/json" }, + }); + const data = await response.json(); + + return data.data; + }, + }); + + return platformMeQuery; +}; diff --git a/packages/platform/types/oauth-clients.ts b/packages/platform/types/oauth-clients.ts index 9666ae8945..1613c18997 100644 --- a/packages/platform/types/oauth-clients.ts +++ b/packages/platform/types/oauth-clients.ts @@ -47,6 +47,7 @@ export const userSchemaResponse = z.object({ timeZone: z.string().default("Europe/London"), username: z.string(), organizationId: z.number().nullable(), + organization: z.object({ isPlatform: z.boolean(), id: z.number() }).optional(), }); export type UserResponse = z.infer;