Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions apps/web/pages/api/trpc/appBasecamp3/[trpc].ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,47 +1,54 @@
import type { NextApiRequest } from "next";
import { z } from "zod";

import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";

import { TRPCError } from "@trpc/server";

import type { TProjectMutationInputSchema } from "./projectMutation.schema";

interface ProjectMutationHandlerOptions {
ctx: {
prisma: PrismaClient;
user: NonNullable<TrpcSessionUser>;
};
input: TProjectMutationInputSchema;
}

interface IDock {
id: number;
name: string;
}
export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHandlerOptions) => {

const ZProjectMutationInputSchema = z.object({ projectId: z.string() });

async function handler(req: NextApiRequest) {
const userId = req.session?.user?.id;
if (!userId) {
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
}

const parsed = ZProjectMutationInputSchema.safeParse(req.body ?? {});
if (!parsed.success) {
throw new HttpError({
statusCode: 400,
message: "Invalid request body",
});
}
const { projectId } = parsed.data;

const { user_agent } = await getAppKeysFromSlug("basecamp3");
const { user, prisma } = ctx;

const { projectId } = input;
const credential = await prisma.credential.findFirst({
where: {
userId: user?.id,
},
where: { userId },
select: credentialForCalendarServiceSelect,
});

if (!credential) {
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
throw new HttpError({ statusCode: 403, message: "No credential found for user" });
}

let credentialKey = credential.key as BasecampToken;

if (credentialKey.expires_at < Date.now()) {
credentialKey = (await refreshAccessToken(credential)) as BasecampToken;
}
// get schedule id

const basecampUserId = credentialKey.account.id;
const scheduleResponse = await fetch(
`https://3.basecampapi.com/${basecampUserId}/projects/${projectId}.json`,
Expand All @@ -52,12 +59,22 @@ export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHand
},
}
);

if (!scheduleResponse.ok) {
throw new HttpError({ statusCode: 400, message: "Failed to fetch project details" });
}

const scheduleJson = await scheduleResponse.json();
const scheduleId = scheduleJson.dock.find((dock: IDock) => dock.name === "schedule").id;

await prisma.credential.update({
where: { id: credential.id },
data: { key: { ...credentialKey, projectId: Number(projectId), scheduleId } },
});

return { message: "Updated project successfully" };
};
}

export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
import type { NextApiRequest } from "next";

import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";

import { TRPCError } from "@trpc/server";
import { HttpError } from "@calcom/lib/http-error";

interface ProjectsHandlerOptions {
ctx: {
prisma: PrismaClient;
user: NonNullable<TrpcSessionUser>;
};
}
async function handler(req: NextApiRequest) {
const userId = req.session?.user?.id;
if (!userId) {
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
}

export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => {
const { user_agent } = await getAppKeysFromSlug("basecamp3");
const { user, prisma } = ctx;

const credential = await prisma.credential.findFirst({
where: {
userId: user?.id,
},
where: { userId },
select: credentialForCalendarServiceSelect,
});
Comment on lines 20 to 23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Filter credential by Basecamp3 app to avoid cross-app leakage.

Without appId, this may pick an arbitrary credential for the user. Filter to the Basecamp3 credential.

-  const credential = await prisma.credential.findFirst({
-    where: { userId },
-    select: credentialForCalendarServiceSelect,
-  });
+  const credential = await prisma.credential.findFirst({
+    where: { userId, appId: "basecamp3" },
+    select: credentialForCalendarServiceSelect,
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const credential = await prisma.credential.findFirst({
where: {
userId: user?.id,
},
where: { userId },
select: credentialForCalendarServiceSelect,
});
const credential = await prisma.credential.findFirst({
where: { userId, appId: "basecamp3" },
select: credentialForCalendarServiceSelect,
});
🤖 Prompt for AI Agents
In packages/app-store/basecamp3/api/projects.ts around lines 20 to 23, the
Prisma query that fetches a credential only filters by userId which can return a
credential for a different app; update the where clause to also filter for the
Basecamp3 app (e.g. add appId: 'basecamp3' or the appropriate constant/enum
value used in the codebase) while keeping the same select
(credentialForCalendarServiceSelect) so only the Basecamp3 credential for that
user is returned.


if (!credential) {
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
throw new HttpError({ statusCode: 403, message: "No credential found for user" });
}

let credentialKey = credential.key as BasecampToken;

if (!credentialKey.account) {
return;
return { currentProject: null, projects: [] };
}

if (credentialKey.expires_at < Date.now()) {
credentialKey = (await refreshAccessToken(credential)) as BasecampToken;
}

const url = `${credentialKey.account.href}/projects.json`;

const resp = await fetch(url, {
headers: { "User-Agent": user_agent as string, Authorization: `Bearer ${credentialKey.access_token}` },
});

if (!resp.ok) {
throw new HttpError({ statusCode: 400, message: "Failed to fetch Basecamp projects" });
}

const projects = await resp.json();
return { currentProject: credentialKey.projectId, projects };
};
}

export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import { useState, useEffect } from "react";

import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
import { trpc } from "@calcom/trpc/react";
import { Select } from "@calcom/ui/components/form";

const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({}) => {
const [projects, setProjects] = useState();
type Basecamp3Project = { label: string; value: string };

const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = () => {
const [projects, setProjects] = useState<Basecamp3Project[] | undefined>();
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
const { data } = trpc.viewer.appBasecamp3.projects.useQuery();
const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation();

useEffect(
function refactorMeWithoutEffect() {
setSelectedProject({
value: data?.projects.currentProject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
});
setProjects(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?.projects?.map((project: any) => {
return {
value: project.id,
label: project.name,
};

useEffect(() => {
async function loadProjects() {
const res = await fetch("/api/integrations/basecamp3/projects");
if (!res.ok) return;
const json = await res.json();
const currentProjectId = json?.currentProject;
const options: Basecamp3Project[] = json?.projects?.map(
(project: { id: number | string; name: string }) => ({
value: String(project.id),
label: project.name,
})
);
},
[data]
);
if (!options) return;
setProjects(options);
if (currentProjectId) {
const match = options.find((project: Basecamp3Project) => project.value === String(currentProjectId));
if (match) setSelectedProject(match);
}
}

loadProjects();
}, []);

async function handleProjectChange(project: { label: string; value: string } | null) {
if (!project) return;

await fetch("/api/integrations/basecamp3/projectMutation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ projectId: project.value }),
});
setSelectedProject(project);
}

return (
<div className="mt-2 text-sm">
Expand All @@ -41,12 +54,7 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({}) => {
options={projects}
isLoading={!projects}
className="md:min-w-[120px]"
onChange={(project) => {
if (project) {
setProject.mutate({ projectId: project?.value.toString() });
setSelectedProject(project);
}
}}
onChange={handleProjectChange}
value={selectedProject}
/>
</div>
Expand Down
26 changes: 0 additions & 26 deletions packages/trpc/server/routers/apps/basecamp3/_router.ts

This file was deleted.

This file was deleted.

2 changes: 0 additions & 2 deletions packages/trpc/server/routers/viewer/_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";

import { router, mergeRouters } from "../../trpc";
import app_Basecamp3 from "../apps/basecamp3/_router";
import app_RoutingForms from "../apps/routing-forms/_router";
import { loggedInViewerRouter } from "../loggedInViewer/_router";
import { publicViewerRouter } from "../publicViewer/_router";
Expand Down Expand Up @@ -79,7 +78,6 @@ export const viewerRouter = router({
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
// After that there would just one merge call here for all the apps.
appRoutingForms: app_RoutingForms,
appBasecamp3: app_Basecamp3,
features: featureFlagRouter,
users: userAdminRouter,
oAuth: oAuthRouter,
Expand Down
Loading