From 8c0aba82e58475b91d14de5ffdf5b6628650ce0b Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Mon, 2 Oct 2023 19:30:25 +0530 Subject: [PATCH] fix: add authorisation for tags actions (#897) * poc: use server session and api key validation on deletion * feat: use server session and api key validation on deletion and creation * feat: packages/lib/apiKey for apiKey services and auth * shubham/auth-for-api-key * fix: caching * feat: handle authorization for tag creation, updation & deletion * fix: use cached wrapper * fix: club caching methods and use authzn errors * feat: add caching in canUserAccessApiKey * fix: suggrsted changes and authzn for response as well * fix: work on suggested changes * fix broken lock file --------- Co-authored-by: Matthias Nannt --- .../(attributeSection)/AttributesSection.tsx | 2 +- .../(responseSection)/ResponseSection.tsx | 2 +- .../[environmentId]/settings/tags/actions.ts | 25 ++++++++++- .../[environmentId]/settings/tags/page.tsx | 2 +- .../surveys/[surveyId]/(analysis)/data.ts | 2 +- .../(analysis)/responses/actions.ts | 42 ++++++++++++++++--- .../components/ResponseTagsWrapper.tsx | 10 ++--- .../[surveyId]/(analysis)/responses/page.tsx | 2 +- .../[surveyId]/(analysis)/summary/page.tsx | 2 +- .../v1/client/responses/[responseId]/route.ts | 2 +- apps/web/app/api/v1/client/responses/route.ts | 2 +- .../responses/[responseId]/route.ts | 2 +- .../app/api/v1/management/responses/route.ts | 2 +- packages/lib/apiKey/auth.ts | 4 +- packages/lib/environment/auth.ts | 4 +- packages/lib/response/auth.ts | 28 +++++++++++++ .../response.ts => response/service.ts} | 4 +- packages/lib/services/survey.ts | 2 +- packages/lib/services/tagOnResponse.ts | 5 ++- packages/lib/tag/auth.ts | 24 +++++++++++ .../lib/{services/tag.ts => tag/service.ts} | 14 +++++++ packages/lib/tagOnResponse/auth.ts | 24 +++++++++++ pnpm-lock.yaml | 36 +++++----------- 23 files changed, 190 insertions(+), 52 deletions(-) create mode 100644 packages/lib/response/auth.ts rename packages/lib/{services/response.ts => response/service.ts} (98%) create mode 100644 packages/lib/tag/auth.ts rename packages/lib/{services/tag.ts => tag/service.ts} (93%) create mode 100644 packages/lib/tagOnResponse/auth.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(attributeSection)/AttributesSection.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(attributeSection)/AttributesSection.tsx index c0f8a70d51..3c4da368f8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(attributeSection)/AttributesSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(attributeSection)/AttributesSection.tsx @@ -4,7 +4,7 @@ import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { capitalizeFirstLetter } from "@/lib/utils"; import { getPerson } from "@formbricks/lib/services/person"; -import { getResponsesByPersonId } from "@formbricks/lib/services/response"; +import { getResponsesByPersonId } from "@formbricks/lib/response/service"; import { getSessionCount } from "@formbricks/lib/services/session"; export default async function AttributesSection({ personId }: { personId: string }) { diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx index da724a130a..60f73bced7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx @@ -1,5 +1,5 @@ import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline"; -import { getResponsesByPersonId } from "@formbricks/lib/services/response"; +import { getResponsesByPersonId } from "@formbricks/lib/response/service"; import { getSurveys } from "@formbricks/lib/services/survey"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/tags/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/tags/actions.ts index f0911442b1..29d59b4574 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/tags/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/tags/actions.ts @@ -1,15 +1,38 @@ "use server"; -import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/services/tag"; +import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service"; +import { canUserAccessTag } from "@formbricks/lib/tag/auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; export const deleteTagAction = async (tagId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessTag(session.user.id, tagId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await deleteTag(tagId); }; export const updateTagNameAction = async (tagId: string, name: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessTag(session.user.id, tagId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await updateTagName(tagId, name); }; export const mergeTagsAction = async (originalTagId: string, newTagId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorizedForOld = await canUserAccessTag(session.user.id, originalTagId); + const isAuthorizedForNew = await canUserAccessTag(session.user.id, newTagId); + if (!isAuthorizedForOld || !isAuthorizedForNew) throw new AuthorizationError("Not authorized"); + return await mergeTags(originalTagId, newTagId); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/tags/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/tags/page.tsx index d225b570ef..b113d9f238 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/tags/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/tags/page.tsx @@ -1,7 +1,7 @@ import EditTagsWrapper from "./EditTagsWrapper"; import SettingsTitle from "../SettingsTitle"; import { getEnvironment } from "@formbricks/lib/services/environment"; -import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag"; +import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { getTagsOnResponsesCount } from "@formbricks/lib/services/tagOnResponse"; export default async function MembersSettingsPage({ params }) { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts index 606ed5bf65..bd86c900a1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts @@ -1,6 +1,6 @@ import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getSurveyResponses } from "@formbricks/lib/services/response"; +import { getSurveyResponses } from "@formbricks/lib/response/service"; import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey"; import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts index f11c261a6f..e774c9d3ee 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts @@ -1,9 +1,15 @@ "use server"; -import { deleteResponse } from "@formbricks/lib/services/response"; +import { deleteResponse } from "@formbricks/lib/response/service"; import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/services/responseNote"; -import { createTag } from "@formbricks/lib/services/tag"; -import { addTagToRespone, deleteTagFromResponse } from "@formbricks/lib/services/tagOnResponse"; +import { createTag } from "@formbricks/lib/tag/service"; +import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/services/tagOnResponse"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; +import { canUserAccessResponse } from "@formbricks/lib/response/auth"; +import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth"; export const updateResponseNoteAction = async (responseNoteId: string, text: string) => { await updateResponseNote(responseNoteId, text); @@ -14,17 +20,41 @@ export const resolveResponseNoteAction = async (responseNoteId: string) => { }; export const deleteResponseAction = async (responseId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessResponse(session.user.id, responseId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await deleteResponse(responseId); }; export const createTagAction = async (environmentId: string, tagName: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await createTag(environmentId, tagName); }; -export const addTagToResponeAction = async (responseId: string, tagId: string) => { +export const createTagToResponeAction = async (responseId: string, tagId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await addTagToRespone(responseId, tagId); }; -export const removeTagFromResponseAction = async (responseId: string, tagId: string) => { - return await deleteTagFromResponse(responseId, tagId); +export const deleteTagOnResponseAction = async (responseId: string, tagId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await deleteTagOnResponse(responseId, tagId); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx index 56878740de..201b544b27 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx @@ -9,9 +9,9 @@ import { useRouter } from "next/navigation"; import { Button } from "@formbricks/ui"; import { TTag } from "@formbricks/types/v1/tags"; import { - addTagToResponeAction, + createTagToResponeAction, createTagAction, - removeTagFromResponseAction, + deleteTagOnResponseAction, } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions"; interface ResponseTagsWrapperProps { @@ -38,7 +38,7 @@ const ResponseTagsWrapper: React.FC = ({ const onDelete = async (tagId: string) => { try { - await removeTagFromResponseAction(responseId, tagId); + await deleteTagOnResponseAction(responseId, tagId); router.refresh(); } catch (e) { @@ -89,7 +89,7 @@ const ResponseTagsWrapper: React.FC = ({ tagName: tag.name, }, ]); - addTagToResponeAction(responseId, tag.id).then(() => { + createTagToResponeAction(responseId, tag.id).then(() => { setSearchValue(""); setOpen(false); router.refresh(); @@ -121,7 +121,7 @@ const ResponseTagsWrapper: React.FC = ({ }, ]); - addTagToResponeAction(responseId, tagId).then(() => { + createTagToResponeAction(responseId, tagId).then(() => { setSearchValue(""); setOpen(false); router.refresh(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index e67110f3b5..49bcc05eb5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -8,7 +8,7 @@ import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constant import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner"; import { getEnvironment } from "@formbricks/lib/services/environment"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag"; +import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; export default async function Page({ params }) { const session = await getServerSession(authOptions); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index 37b9434bfe..d1f1172d33 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -7,7 +7,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/services/environment"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag"; +import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { getServerSession } from "next-auth"; export default async function Page({ params }) { diff --git a/apps/web/app/api/v1/client/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/responses/[responseId]/route.ts index fd92d2b780..aba81b2fb4 100644 --- a/apps/web/app/api/v1/client/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/responses/[responseId]/route.ts @@ -2,7 +2,7 @@ import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; -import { updateResponse } from "@formbricks/lib/services/response"; +import { updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/services/survey"; import { ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index a9898b1b92..aac5cbbabc 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { InvalidInputError } from "@formbricks/types/v1/errors"; import { capturePosthogEvent } from "@formbricks/lib/posthogServer"; -import { createResponse } from "@formbricks/lib/services/response"; +import { createResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/services/survey"; import { getTeamDetails } from "@formbricks/lib/services/teamDetails"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses"; diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 42375f3ed9..7941a82eea 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/lib/api/response"; import { NextResponse } from "next/server"; import { transformErrorToDetails } from "@/lib/api/validator"; -import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/services/response"; +import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service"; import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getSurvey } from "@formbricks/lib/services/survey"; diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts index 087cc40809..3fab5f3a3b 100644 --- a/apps/web/app/api/v1/management/responses/route.ts +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -1,5 +1,5 @@ import { responses } from "@/lib/api/response"; -import { getEnvironmentResponses } from "@formbricks/lib/services/response"; +import { getEnvironmentResponses } from "@formbricks/lib/response/service"; import { authenticateRequest } from "@/app/api/v1/auth"; import { DatabaseError } from "@formbricks/types/v1/errors"; diff --git a/packages/lib/apiKey/auth.ts b/packages/lib/apiKey/auth.ts index 6f565c7a98..6699514ddc 100644 --- a/packages/lib/apiKey/auth.ts +++ b/packages/lib/apiKey/auth.ts @@ -1,3 +1,5 @@ +import { ZId } from "@formbricks/types/v1/environment"; +import { validateInputs } from "../utils/validate"; import { hasUserEnvironmentAccess } from "../environment/auth"; import { getApiKey } from "./service"; import { unstable_cache } from "next/cache"; @@ -5,7 +7,7 @@ import { unstable_cache } from "next/cache"; export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Promise => await unstable_cache( async () => { - if (!userId) return false; + validateInputs([userId, ZId], [apiKeyId, ZId]); const apiKeyFromServer = await getApiKey(apiKeyId); if (!apiKeyFromServer) return false; diff --git a/packages/lib/environment/auth.ts b/packages/lib/environment/auth.ts index c3dc535659..4a693e6ed0 100644 --- a/packages/lib/environment/auth.ts +++ b/packages/lib/environment/auth.ts @@ -1,10 +1,12 @@ import { prisma } from "@formbricks/database"; +import { ZId } from "@formbricks/types/v1/environment"; import { unstable_cache } from "next/cache"; +import { validateInputs } from "../utils/validate"; export const hasUserEnvironmentAccess = async (userId: string, environmentId: string) => { return await unstable_cache( async () => { - if (!userId) return false; + validateInputs([userId, ZId], [environmentId, ZId]); const environment = await prisma.environment.findUnique({ where: { id: environmentId, diff --git a/packages/lib/response/auth.ts b/packages/lib/response/auth.ts new file mode 100644 index 0000000000..48ea212553 --- /dev/null +++ b/packages/lib/response/auth.ts @@ -0,0 +1,28 @@ +import { ZId } from "@formbricks/types/v1/environment"; +import { validateInputs } from "../utils/validate"; +import { hasUserEnvironmentAccess } from "../environment/auth"; +import { getResponse, getResponseCacheTag } from "./service"; +import { unstable_cache } from "next/cache"; +import { getSurvey } from "../services/survey"; + +export const canUserAccessResponse = async (userId: string, responseId: string): Promise => + await unstable_cache( + async () => { + validateInputs([userId, ZId], [responseId, ZId]); + + if (!userId) return false; + + const response = await getResponse(responseId); + if (!response) return false; + + const survey = await getSurvey(response.surveyId); + if (!survey) return false; + + const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId); + if (!hasAccessToEnvironment) return false; + + return true; + }, + [`users-${userId}-responses-${responseId}`], + { revalidate: 30 * 60, tags: [getResponseCacheTag(responseId)] } + )(); // 30 minutes diff --git a/packages/lib/services/response.ts b/packages/lib/response/service.ts similarity index 98% rename from packages/lib/services/response.ts rename to packages/lib/response/service.ts index 58b6f5b015..b0f2595826 100644 --- a/packages/lib/services/response.ts +++ b/packages/lib/response/service.ts @@ -12,7 +12,7 @@ import { TTag } from "@formbricks/types/v1/tags"; import { Prisma } from "@prisma/client"; import { cache } from "react"; import "server-only"; -import { getPerson, transformPrismaPerson } from "./person"; +import { getPerson, transformPrismaPerson } from "../services/person"; import { captureTelemetry } from "../telemetry"; import { validateInputs } from "../utils/validate"; import { ZId } from "@formbricks/types/v1/environment"; @@ -78,6 +78,8 @@ const responseSelection = { export const getResponsesCacheTag = (surveyId: string) => `surveys-${surveyId}-responses`; +export const getResponseCacheTag = (responseId: string) => `responses-${responseId}`; + export const getResponsesByPersonId = async (personId: string): Promise | null> => { validateInputs([personId, ZId]); try { diff --git a/packages/lib/services/survey.ts b/packages/lib/services/survey.ts index ede996055f..c7466cdb00 100644 --- a/packages/lib/services/survey.ts +++ b/packages/lib/services/survey.ts @@ -15,7 +15,7 @@ import { z } from "zod"; import { captureTelemetry } from "../telemetry"; import { validateInputs } from "../utils/validate"; import { getDisplaysCacheTag } from "./displays"; -import { getResponsesCacheTag } from "./response"; +import { getResponsesCacheTag } from "../response/service"; // surveys cache key and tags const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`; diff --git a/packages/lib/services/tagOnResponse.ts b/packages/lib/services/tagOnResponse.ts index 64908b53a5..4e22c16afb 100644 --- a/packages/lib/services/tagOnResponse.ts +++ b/packages/lib/services/tagOnResponse.ts @@ -2,6 +2,9 @@ import { prisma } from "@formbricks/database"; import { TTagsCount } from "@formbricks/types/v1/tags"; import { cache } from "react"; +export const getTagOnResponseCacheTag = (tagId: string, responseId: string) => + `tagsOnResponse-${tagId}-${responseId}`; + export const addTagToRespone = async (responseId: string, tagId: string) => { try { const tagOnResponse = await prisma.tagsOnResponses.create({ @@ -16,7 +19,7 @@ export const addTagToRespone = async (responseId: string, tagId: string) => { } }; -export const deleteTagFromResponse = async (responseId: string, tagId: string) => { +export const deleteTagOnResponse = async (responseId: string, tagId: string) => { try { const deletedTag = await prisma.tagsOnResponses.delete({ where: { diff --git a/packages/lib/tag/auth.ts b/packages/lib/tag/auth.ts new file mode 100644 index 0000000000..d9c3b85543 --- /dev/null +++ b/packages/lib/tag/auth.ts @@ -0,0 +1,24 @@ +import { validateInputs } from "../utils/validate"; +import { hasUserEnvironmentAccess } from "../environment/auth"; +import { getTag } from "./service"; +import { unstable_cache } from "next/cache"; +import { ZId } from "@formbricks/types/v1/environment"; + +export const canUserAccessTag = async (userId: string, tagId: string): Promise => + await unstable_cache( + async () => { + validateInputs([userId, ZId], [tagId, ZId]); + + const tag = await getTag(tagId); + if (!tag) return false; + + const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, tag.environmentId); + if (!hasAccessToEnvironment) return false; + + return true; + }, + [`${userId}-${tagId}`], + { + revalidate: 30 * 60, // 30 minutes + } + )(); diff --git a/packages/lib/services/tag.ts b/packages/lib/tag/service.ts similarity index 93% rename from packages/lib/services/tag.ts rename to packages/lib/tag/service.ts index a8562fd89c..32028f4197 100644 --- a/packages/lib/services/tag.ts +++ b/packages/lib/tag/service.ts @@ -16,6 +16,20 @@ export const getTagsByEnvironmentId = cache(async (environmentId: string): Promi } }); +export const getTag = async (tagId: string): Promise => { + try { + const tag = await prisma.tag.findUnique({ + where: { + id: tagId, + }, + }); + + return tag; + } catch (error) { + throw error; + } +}; + export const createTag = async (environmentId: string, name: string): Promise => { try { const tag = await prisma.tag.create({ diff --git a/packages/lib/tagOnResponse/auth.ts b/packages/lib/tagOnResponse/auth.ts new file mode 100644 index 0000000000..2b54f4443e --- /dev/null +++ b/packages/lib/tagOnResponse/auth.ts @@ -0,0 +1,24 @@ +import { validateInputs } from "../utils/validate"; +import { unstable_cache } from "next/cache"; +import { ZId } from "@formbricks/types/v1/environment"; +import { canUserAccessResponse } from "../response/auth"; +import { canUserAccessTag } from "../tag/auth"; +import { getTagOnResponseCacheTag } from "../services/tagOnResponse"; + +export const canUserAccessTagOnResponse = async ( + userId: string, + tagId: string, + responseId: string +): Promise => + await unstable_cache( + async () => { + validateInputs([userId, ZId], [tagId, ZId], [responseId, ZId]); + + const isAuthorizedForTag = await canUserAccessTag(userId, tagId); + const isAuthorizedForResponse = await canUserAccessResponse(userId, responseId); + + return isAuthorizedForTag && isAuthorizedForResponse; + }, + [`users-${userId}-tagOnResponse-${tagId}-${responseId}`], + { revalidate: 30 * 60, tags: [getTagOnResponseCacheTag(tagId, responseId)] } + )(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1d37603a1..d479d83082 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -434,7 +438,7 @@ importers: version: 9.0.0(eslint@8.50.0) eslint-config-turbo: specifier: latest - version: 1.10.14(eslint@8.50.0) + version: 1.8.8(eslint@8.50.0) eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.50.0) @@ -10673,13 +10677,13 @@ packages: resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==} dev: true - /eslint-config-turbo@1.10.14(eslint@8.50.0): - resolution: {integrity: sha512-ZeB+IcuFXy1OICkLuAplVa0euoYbhK+bMEQd0nH9+Lns18lgZRm33mVz/iSoH9VdUzl/1ZmFmoK+RpZc+8R80A==} + /eslint-config-turbo@1.8.8(eslint@8.50.0): + resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.50.0 - eslint-plugin-turbo: 1.10.14(eslint@8.50.0) + eslint-plugin-turbo: 1.8.8(eslint@8.50.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -10885,12 +10889,11 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.10.14(eslint@8.50.0): - resolution: {integrity: sha512-sBdBDnYr9AjT1g4lR3PBkZDonTrMnR4TvuGv5W0OiF7z9az1rI68yj2UHJZvjkwwcGu5mazWA1AfB0oaagpmfg==} + /eslint-plugin-turbo@1.8.8(eslint@8.50.0): + resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} peerDependencies: eslint: '>6.6.0' dependencies: - dotenv: 16.0.3 eslint: 8.50.0 dev: true @@ -22036,70 +22039,58 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.13: resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} - cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.13: resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} - cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.13: resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} - cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.13: resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} - cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.13: resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} - cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.13: resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} - cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.13: resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} hasBin: true + requiresBuild: true optionalDependencies: turbo-darwin-64: 1.10.13 turbo-darwin-arm64: 1.10.13 @@ -22107,7 +22098,6 @@ packages: turbo-linux-arm64: 1.10.13 turbo-windows-64: 1.10.13 turbo-windows-arm64: 1.10.13 - dev: true /tween-functions@1.2.0: @@ -23974,7 +23964,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false