diff --git a/apps/web/pages/api/trpc/appBasecamp3/[trpc].ts b/apps/web/pages/api/trpc/appBasecamp3/[trpc].ts deleted file mode 100644 index 3c29f5f0292a6d..00000000000000 --- a/apps/web/pages/api/trpc/appBasecamp3/[trpc].ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler"; -import appBasecamp3 from "@calcom/trpc/server/routers/apps/basecamp3/_router"; - -export default createNextApiHandler(appBasecamp3); diff --git a/packages/trpc/server/routers/apps/basecamp3/projectMutation.handler.ts b/packages/app-store/basecamp3/api/projectMutation.ts similarity index 56% rename from packages/trpc/server/routers/apps/basecamp3/projectMutation.handler.ts rename to packages/app-store/basecamp3/api/projectMutation.ts index 21ad4a30f629a6..284ee178bef60d 100644 --- a/packages/trpc/server/routers/apps/basecamp3/projectMutation.handler.ts +++ b/packages/app-store/basecamp3/api/projectMutation.ts @@ -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; - }; - 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`, @@ -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) }), +}); diff --git a/packages/trpc/server/routers/apps/basecamp3/projects.handler.ts b/packages/app-store/basecamp3/api/projects.ts similarity index 55% rename from packages/trpc/server/routers/apps/basecamp3/projects.handler.ts rename to packages/app-store/basecamp3/api/projects.ts index c6f532c3112bb7..03ba67679738ad 100644 --- a/packages/trpc/server/routers/apps/basecamp3/projects.handler.ts +++ b/packages/app-store/basecamp3/api/projects.ts @@ -1,34 +1,35 @@ +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; - }; -} +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, }); + 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()) { @@ -36,10 +37,18 @@ export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => { } 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) }), +}); diff --git a/packages/app-store/basecamp3/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/basecamp3/components/EventTypeAppSettingsInterface.tsx index 3447e835beefb3..132793d7c7b697 100644 --- a/packages/app-store/basecamp3/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/basecamp3/components/EventTypeAppSettingsInterface.tsx @@ -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(); const [selectedProject, setSelectedProject] = useState(); - 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 (
@@ -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} />
diff --git a/packages/trpc/server/routers/apps/basecamp3/_router.ts b/packages/trpc/server/routers/apps/basecamp3/_router.ts deleted file mode 100644 index e3f9d7159524ea..00000000000000 --- a/packages/trpc/server/routers/apps/basecamp3/_router.ts +++ /dev/null @@ -1,26 +0,0 @@ -import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure"; -import { router } from "@calcom/trpc/server/trpc"; - -import { ZProjectMutationInputSchema } from "./projectMutation.schema"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const UNSTABLE_HANDLER_CACHE: any = {}; - -const appBasecamp3 = router({ - projects: authedProcedure.query(async ({ ctx }) => { - if (!UNSTABLE_HANDLER_CACHE.projects) { - UNSTABLE_HANDLER_CACHE.projects = await import("./projects.handler").then((mod) => mod.projectHandler); - } - return UNSTABLE_HANDLER_CACHE.projects({ ctx }); - }), - projectMutation: authedProcedure.input(ZProjectMutationInputSchema).mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.projectMutation) { - UNSTABLE_HANDLER_CACHE.projectMutation = await import("./projectMutation.handler").then( - (mod) => mod.projectMutationHandler - ); - } - return UNSTABLE_HANDLER_CACHE.projectMutation({ ctx, input }); - }), -}); - -export default appBasecamp3; diff --git a/packages/trpc/server/routers/apps/basecamp3/projectMutation.schema.ts b/packages/trpc/server/routers/apps/basecamp3/projectMutation.schema.ts deleted file mode 100644 index b23985de45d5b2..00000000000000 --- a/packages/trpc/server/routers/apps/basecamp3/projectMutation.schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const ZProjectMutationInputSchema = z.object({ - projectId: z.string(), -}); - -export type TProjectMutationInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/_router.tsx b/packages/trpc/server/routers/viewer/_router.tsx index 521d2ad1039908..1f9ebee9139d6c 100644 --- a/packages/trpc/server/routers/viewer/_router.tsx +++ b/packages/trpc/server/routers/viewer/_router.tsx @@ -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"; @@ -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,