fix: platform me data for dashboard (#16210)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,8 +29,19 @@ export class MeController {
|
||||
@Get("/")
|
||||
@Permissions([PROFILE_READ])
|
||||
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -67,12 +67,15 @@ export class UsersRepository {
|
||||
}
|
||||
|
||||
async findByIdWithProfile(userId: number): Promise<UserWithProfile | null> {
|
||||
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 } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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<typeof userSchemaResponse>;
|
||||
|
||||
Reference in New Issue
Block a user