fix: platform me data for dashboard (#16210)

This commit is contained in:
Morgan
2024-08-15 10:12:31 +00:00
committed by GitHub
parent 84606543ae
commit 8477128605
10 changed files with 150 additions and 227 deletions
@@ -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();
});
});
+13 -2
View File
@@ -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,
+12 -1
View File
@@ -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 } } },
},
},
});
}
+18 -207
View File
@@ -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;
};
+1
View File
@@ -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>;